tstorms
tstorms

Reputation: 5001

REST interface implementation for exposing Neo4j nodes

I'm representing my nodes as domain entities. I'd like to expose my nodes via a REST interface using GET to query by id, POST to save a node with its related nodes and PUT to update a node and its related nodes. GET is working just fine, but I'm having an implementation question about the POST and PUT case. Let me illustrate the problem as code often says a lot more than words.

In this example, there are two related node types represented as domain entities. A Collaboration can have multiple tags and a tags can belong to multiple collaborations. So we're having a many to many relationship. They both share the same base class NodeBacked which basically serves as a wrapper for the underlying node.

NodeBacked

abstract class NodeBacked {

    private Node node;

    public NodeBacked(final Node node) {
        this.node = node;
    }

    public Long getId() {
        return this.node.getId();
    }

    @Override
    public int hashCode() {
        return this.node.hashCode();
    }   

    @JsonIgnore
    public Node getNode() {
        return this.node;
    }

}

Collaboration

public class Collaboration extends NodeBacked {

    public Collaboration(final Node node) {
        super(node);
    }

    // Leaving out some properties for clearness

    @JsonProperty(NAME_JSON)
    public String getName() {
        return (String) getNode().getProperty(NAME);
    }   

    @JsonProperty(TAGS_JSON)
    public Iterable<Tag> getTags() {
        return new IterableWrapper<Tag, Path>(Traversal.description().breadthFirst()
                .relationships(Relationships.HAS, Direction.OUTGOING).uniqueness(Uniqueness.NODE_GLOBAL)
                .evaluator(Evaluators.atDepth(1)).evaluator(Evaluators.excludeStartPosition()).traverse(getNode())) 

{
            @Override
            protected Tag underlyingObjectToObject(final Path path) {
                return new Tag(path.endNode());
            }
        };
    }

    public void setName(final String name) {
        final Index<Node> index = getNode().getGraphDatabase().index().forNodes(Indexes.NAMES);
        getNode().setProperty(NAME, name);
        if (StringUtils.isNotEmpty(getName())) {
            index.remove(getNode(), NAME, name);
        }
        index.add(getNode(), NAME, name);
    }

    public void addTag(final Tag tag) {
        if (!Traversal.description().breadthFirst().relationships(Relationships.HAS, Direction.OUTGOING)
                .uniqueness(Uniqueness.NODE_GLOBAL).evaluator(Evaluators.atDepth(1))
                .evaluator(Evaluators.excludeStartPosition())
                .evaluator(Evaluators.includeWhereEndNodeIs(tag.getNode())).traverse(getNode()).iterator().hasNext

()) {
            getNode().createRelationshipTo(tag.getNode(), Relationships.HAS);
        }
    }

    @Override
    public boolean equals(final Object o) {
        return o instanceof Collaboration && getNode().equals(((Collaboration) o).getNode());
    }

}

Tag

public class Tag extends NodeBacked {

    public Tag(final Node node) {
        super(node);
    }

    @JsonProperty(NAME_JSON)
    public String getName() {
        return (String) getNode().getProperty(NAME);
    }

    public void setName(final String name) {
        final Index<Node> index = getNode().getGraphDatabase().index().forNodes(Indexes.NAMES);
        getNode().setProperty(NAME, name);
        if (StringUtils.isNotEmpty(getName())) {
            index.remove(getNode(), NAME, name);
        }
        index.add(getNode(), NAME, name);
    }

    @JsonProperty(COLLABORATIONS_JSON)
    @JsonSerialize(using = SimpleCollaborationSerializer.class)
    private Iterable<Collaboration> getCollaborations(int depth) {
        return new IterableWrapper<Collaboration, Path>(Traversal.description().breadthFirst()
                .relationships(Relationships.HAS, Direction.INCOMING).uniqueness(Uniqueness.NODE_GLOBAL)
                .evaluator(Evaluators.atDepth(1)).evaluator(Evaluators.excludeStartPosition()).traverse(getNode())) 

{
            @Override
            protected Collaboration underlyingObjectToObject(final Path path) {
                return new Collaboration(path.endNode());
            }
        };
    }

    @Override
    public boolean equals(final Object o) {
        return o instanceof Tag && getNode().equals(((Tag) o).getNode());
    }

}

I'm exposing the Collaboration via REST (Spring 3.2) as follows. The MappingJackson2HttpMessageConverter is used to transform POJO's to JSON and visa versa.

@Controller
@RequestMapping(value = CollaborationController.CONTEXT_PATH)
public class CollaborationController {

    public static final String CONTEXT_PATH = "/collaborations";

    @Autowired
    private GraphDatabaseService db;

    @Transactional
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public @ResponseBody Collaboration getCollaboration(final @PathVariable Long id) {
        // should use a service layer but doing this for clearness
        return new Collaboration(db.getNodeById(id));
    }   

}

This works great. The getters query the node for it's properties and a decent JSON string is returned. An example JSON string for GET /collaborations/1:

{
  "name" : "Dummy Collaboration",
  "tags" : [ {
    "name" : "test",
    "id" : 3
  }, {
    "name" : "dummy",
    "id" : 2
  } ],
  "id" : 1
}

So, what's the problem then? Imagine a POST request with a JSON body that looks as follows:

{
  "name" : "Second collaboration",
  "tags" : [ {
    "name" : "tagged"
  } ]
}

The CollaborationController has the following method to handle POST requests:

@Transactional
    @RequestMapping(method = RequestMethod.POST, headers = JSON_CONTENT_TYPE)
    public @ResponseBody ResponseEntity<Collaboration> persist(final @RequestBody Collaboration collaboration, 
            final UriComponentsBuilder builder) {
        final Collaboration collab = new Collaboration(db.createNode(Labels.COLLAB));
        // Problem!!
        collab.setName(collaboration.getName());        

        final HttpHeaders headers = new HttpHeaders();
        headers.setLocation(builder.path(CONTEXT_PATH + "/{id}").buildAndExpand(collab.getId()).toUri());
        return new ResponseEntity<Collaboration>(collab, headers, HttpStatus.CREATED);
    }

The line collab.setName(collaboration.getName()); won't work because the collaboration class does not contain properties of its own and uses getters that directly query the underlying node. In this case there isn't any node available, because the Collaboration should be ransformed from JSON to POJO by Jackson2 with Spring's MappingJackson2HttpMessageConverter. There aren't any properties, so there's nothing to be set...

I'm looking into the cleanest solution for this problem, but haven't found one yet. I could use a POJO (or VO or ...) as incoming parameter for the persist method, but that's not really maintainable. A change of a property would require an update of the Collaboration class as well of the CollaborationVO (the POJO) class.

Suggestions are more than welcome! Spring Data Neo4j solves mainly all of this, but I'm not happy with it's performance. That's the exact reason why I'm trying out another approach.

I hope this explanation is clear enough. Thanks for bearing with me!

Frameworks used:

Upvotes: 4

Views: 495

Answers (1)

JoeG
JoeG

Reputation: 7652

You said:

I could use a POJO (or VO or ...) as incoming parameter for the persist method, 
but that's not really maintainable. 

but I don't see a way around this - although I agree it seems ugly and that there should be a better way; however, I have yet to see one.

An aspect of your approach not discussed is that it exposes the underlying database implementation to the client. Sure, it's serialized as JSON, but does that stop the client from putting together a "Collaboration (or Tag)" object? [Your client might be a javascript client so that this is not an issue, but that doesn't stop some other hypothetical client from constructing one of your objects.] What happens if that client calls the getCollaborations() (or getTags()) method? To me, this is the worse of the two evils.

I would change your existing domain entities to be something like ECollaboration/ETag, and then create pure POJOs for Collaboration/Tag. It is okay for the E classes to know of the POJOs, but not the other way. Thus, the E classes could also all implement an interface with a methods like: "convert()" and "valueOf()" to translate between the API class and the domain class. Actually these methods also help the maintainability a little bit - when something changes you can easily see there is an impact to another object.

Upvotes: 1

Related Questions