Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Work Order Management With Neo4j

DZone's Guide to

Work Order Management With Neo4j

Learn how to build part of a work order management system with Neo4j to so you can get your priorities in shape and, as The Britney says, get to work.

· Database Zone
Free Resource

Find out how Database DevOps helps your team deliver value quicker while keeping your data safe and your organization compliant. Align DevOps for your applications with DevOps for your SQL Server databases to discover the advantages of true Database DevOps, brought to you in partnership with Redgate

I look terrible in a bikini (take my word for it), but I'd love me a Lamborghini. However, in order to afford nice things, we need to do as the song says and get to work... and we need to manage and prioritize that work somehow. Today, I'm going to show you how to build part of a work order management system with Neo4j.

I'm going to build an evented work order model. Let's say our order gets created, then based on what it is, pieces of work need to happen. This work is performed by some provider (whether internal or external) and that work can be broken down into tasks that have dependencies on events that have occurred. How would this look in the graph? Glad you asked:

You may be thinking, "Hey, where are the dependencies between the tasks and the events?" Well, we don't have the events until they occur, and we don't know the order ahead of time. Plus, our dependency may be complicated. If "ex" represents an event, a task may express dependencies as "(e1 and e2) or (e3 but not e4)". So, events 1 and 2 need to have happened, or just event 3 as long as event 4 didn't happen.

If this sounds familiar to you it's because I just blogged about a boolean logic rules engine in a recent blog post. We can take the same ideas, turn them sideways, and find out for any given order what tasks are completed, what tasks remain to be done, and what events are tasks waiting on.

Let's create some sample data and see how this works. First, we'll create some nodes:

    CREATE (o:Order {id:'o1'})
    CREATE (p1:Provider {name:'Provider One'})
    CREATE (p2:Provider {name:'Provider Two'})
    CREATE (w1:Work {id:'w1'})
    CREATE (w2:Work {id:'w2'})
    CREATE (t1:Task {id:'t1', requires:"(e1 & e2) | (e3 & !e4)"})
    CREATE (t2:Task {id:'t2'})
    CREATE (t3:Task {id:'t3'})
    CREATE (e1:Event {id:'e1'})
    CREATE (e4:Event {id:'e4'})

Then, we will connect them together with some relationships:

    CREATE (o)-[:HAS_WORK]->(w1)
    CREATE (o)-[:HAS_WORK]->(w2)
    CREATE (p1)-[:PERFORMS]->(w1)
    CREATE (p2)-[:PERFORMS]->(w2)
    CREATE (w1)-[:HAS_TASK]->(t1)
    CREATE (w2)-[:HAS_TASK]->(t2)
    CREATE (w2)-[:HAS_TASK]->(t3)
    CREATE (e1)-[:FIRST]->(o)
    CREATE (e4)-[:PREVIOUS]->(e1)

This will create a graph with one work order, two providers, two work items, three tasks, and two events — which looks like this:

Tasks 2 and 3 do not have any event dependencies, they can happen at any time, but Task 1 requires some events to have occurred. Events 1 and 4 have occurred, can we proceed with completing this task? Let's build a stored procedure to answer this question. We can start by finding the Order in question in the Graph:

    @Procedure(name = "com.maxdemarzi.order.tasks", mode = Mode.WRITE)
    @Description("CALL com.maxdemarzi.order.tasks(orderId) - get tasks for orders")
    public Stream<MapResult> orderTasks(@Name("orderId") String orderId) throws IOException {
        ArrayList<Map<String, Object>> tasks = new ArrayList<>();

        // We start by finding the order
        Node order = db.findNode(Labels.Order, "id", orderId);
        if (order != null) {

Once we have the order, we want to find all the events that are associated with this order and get their IDs:

            // Create a traversal description that finds all events
            TraversalDescription eventTraversal = db.traversalDescription()
                    .depthFirst().expand(PathExpanders
                            .forTypesAndDirections(
                                    RelationshipTypes.FIRST, Direction.INCOMING,
                                    RelationshipTypes.PREVIOUS, Direction.INCOMING)
                    ).evaluator(Evaluators.excludeStartPosition());

            // Gather all of its event ids in to a Set
            Set<String> eventsIds = new HashSet<>();
            for (Path path : eventTraversal.traverse(order)) {
                String eventId = (String) path.endNode().getProperty("id");
                // If this is a negative event, remove existing event id
                if (eventId.charAt(0) == '-') {
                    eventsIds.remove(eventId.substring(1, eventId.length()));
                } else {
                    eventsIds.add(eventId);
                }
            }

You'll notice I look for negative events. That is events that have an id that starts with a minus sign, for example, "-ex2". If I encounter these I remove the pre-existing event ID from the set of events. In a real event-based workflow system, your event stream should be immutable, so instead of simply deleting events, you would generate events that would undo existing events (using "-ex"). Now that we have the events, let's get the work that needs to be done, the providers who need to do it and the tasks they need to perform:

            // Find the work required to complete the order
            for (Relationship r1 : order.getRelationships(Direction.OUTGOING, RelationshipTypes.HAS_WORK)) {
                Node work = r1.getEndNode();
                // Figure out who will perform this work
                Node provider = work.getSingleRelationship(RelationshipTypes.PERFORMS, Direction.INCOMING).getStartNode();
                String providerName = (String) provider.getProperty("name");

                // For each work item, find the associated tasks
                for (Relationship r2 : work.getRelationships(Direction.OUTGOING, RelationshipTypes.HAS_TASK)) {
                    Node task = r2.getEndNode();
                    Map<String, Object> properties = task.getAllProperties();
                    properties.put("provider", providerName);

For each task, we want to find out if it can be performed independently or if it has some requirements. We will create a boolean expression and get all minimum sum-of-products solutions. Instead of creating Path nodes like in the previous blog post, we will simply save them as a string array property on the node itself, so we only ever have to calculate this once:

                    // If the task has requirements see what is left to be done
                    String requires = (String) task.getProperty("requires", null);
                    if (requires != null) {
                        // Have we already calculated dependencies?
                        String[] paths = (String[])task.getProperty("dependencies", null);
                        if (paths == null) {
                            // Calculate the dependencies and save them, so we only ever do this once.
                            BooleanExpression boEx = new BooleanExpression(requires);
                            boEx.doTabulationMethod();
                            boEx.doQuineMcCluskey();
                            boEx.doPetricksMethod();
                            paths = boEx.getPathExpressions().toArray(new String[]{});
                            task.setProperty("dependencies", paths);
                        } 

Just like before we check each path against the events that have occurred, except this time we will also capture the events that have not happened or the ones that need to be removed for this task to continue.

                        // Check our dependencies against the events of the order
                        ArrayList<HashMap<String, Object>> dependencies = new ArrayList<>();
                        for (String path : paths) {
                            String[] ids = path.split("[!&]");
                            char[] rels = path.replaceAll("[^&^!]", "").toCharArray();
                            Set<String> missing = new HashSet<>();
                            Set<String> remove = new HashSet<>();

                            // Check the first required event in the path
                            if (!eventsIds.contains(ids[0])) {
                                missing.add(ids[0]);
                            }

                            // Check the rest of the events
                            if (ids.length > 1) {
                                for (int i = 0; i < rels.length; i++) {
                                    if (rels[i] == '&') {
                                        if (!eventsIds.contains(ids[1 + i])) {
                                            missing.add(ids[1 + i]);
                                        }
                                    } else {
                                        if (eventsIds.contains(ids[1 + i])) {
                                            remove.add(ids[1 + i]);
                                        }
                                    }
                                }
                            }

Finally, we add these to our properties and then to our task list to be returned from our method.

                            // Add the dependencies
                            HashMap<String, Object> dependency = new HashMap<>();
                            dependency.put("missing", missing);
                            dependency.put("remove", remove);
                            dependencies.add(dependency);
                        }
                        properties.put("dependencies", dependencies);
                    }
                    tasks.add(properties);

Now we can call our stored procedure:

CALL com.maxdemarzi.order.tasks('o1') yield value return value

And see in our answer that Tasks 2 and 3 done by Provider 2 are ready to be performed, but Task 1 done by Provider One requires either Event 2 to occur or Event 3 to occur along with the removal of Event 4.

{
  "provider": "Provider Two",
  "id": "t3"
},
{
  "provider": "Provider Two",
  "id": "t2"
},
{
  "provider": "Provider One",
  "id": "t1",
  "requires": "(e1 & e2) | (e3 & !e4)",
  "dependencies": [
    {
      "missing": [
        "e2"
      ],
      "remove": []
    },
    {
      "missing": [
        "e3"
      ],
      "remove": [
        "e4"
      ]
    }
  ]
}

To add Event 2 to our Order, we can run this Cypher query:

MATCH (order:Order {id:'o1'})<-[:FIRST|PREVIOUS*]-(last:Event) 
WHERE SIZE((last)<-[:PREVIOUS]-()) = 0
CREATE (e2:Event {id:'e2'}) 
CREATE (e2)-[:PREVIOUS]->(last)

And if we run our stored procedure again, we can see that Task 1 can now be performed. So, there you have it. The source code, as always, is on GitHub. Now get to work, sashay, shante!

Align DevOps for your applications with DevOps for your SQL Server databases to increase speed of delivery and keep data safe. Discover true Database DevOps, brought to you in partnership with Redgate

Topics:
database ,tutorial ,neo4j ,work orders ,stored procedures

Published at DZone with permission of Max De Marzi, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}