Matthew,
it's right, we do not have composite indexing across nodes or rels. We
do complex uniquess (or other) checks in the onCreation method of a
node's entity class, preventing the transaction from being committed if
one fails. You could f.e. combine uniquess checks with other validators,
e.g. RegEx, minimum length or the like. In some Neo4j-related projects,
we found that in real-world use-cases, you need a lot of flexibilty when
doing the validation in your REST API.
For your use case
Software.name
Software.version
Software.Organization.name
we'd define two node entity classes Software and Organization like this
(simplified, untested code):
public class Software extends AbstractNode {
public enum Key implements PropertyKey{ name, version, organization }
static {
// Make "name", "version" and "organization" valid properties
EntityContext.registerPropertySet(Software.class,
PropertyView.All, Key.values());
// Define "organization" as being the "name" property of a
related Organization node
// The second argument of the PropertyNotion constructor means
'createIfNotExists'
EntityContext.registerEntityRelation(Software.class,
Organization.class, RelType.HAS, Direction.OUTGOING,
Cardinality.OneToMany, new PropertyNotion(
Organization.Key.name, true));
// Uniquess for Software.name
EntityContext.registerPropertyValidator(Software.class,
Key.name, new TypeUniquenessValidator("Software"));
}
[...]
@Override
public boolean onCreation(SecurityContext securityContext,
ErrorBuffer errorBuffer) {
List<SearchAttribute> searchAttributes = new
LinkedList<SearchAttribute>();
Map<String, String> fields = new
LinkedHashMap<String, String>();
String name =
getStringProperty(Key.name);
String version =
getStringProperty(Key.city);
String org =
getStringProperty(Key.organization);
if (name == null) {
name = "";
}
if (version== null) {
version= "";
}
if (org== null) {
org= "";
}
// add type to search attributes
searchAttributes.add(Search.andExactType(Software.class.getSimpleName()));
// add new fields to search attributes
searchAttributes.add(Search.andExactProperty(Key.name, name));
searchAttributes.add(Search.andExactProperty(Key.version,
version));
searchAttributes.add(Search.andExactProperty(Key.org, org));
fields.put(
Key.name.name(), name);
fields.put(
Key.version.name(), version);
fields.put(
Key.org.name(), org);
// do search
try {
List<GraphObject> results = (List<GraphObject>)
Services.command(securityContext, SearchNodeCommand.class).execute(null,
false, false, searchAttributes);
if (!results.isEmpty()) {
for (Entry<String, String> entry : fields.entrySet()) {
if (StringUtils.isNotBlank(entry.getValue())) {
String id = ((AbstractNode)
results.get(0)).getUuid();
errorBuffer.add(Software.class.getSimpleName(),
new UniqueToken(id, entry.getKey(), entry.getValue()));
}
}
return true;
}
} catch (FrameworkException fex) {
logger.log(Level.WARNING, "Unable to validate uniqueness of
Software");
}
// everything's ok
return false;
}
}
public class Organization extends AbstractNode {
public enum Key implements PropertyKey{ name, software }
static {
EntityContext.registerPropertySet(Organization.class,
PropertyView.All, Key.values());
EntityContext.registerEntityRelation(Organization.class,Software.class,
RelType.HAS, Direction.INCOMING, Cardinality.ManyToOne);
EntityContext.registerPropertyValidator(Organization.class,
Key.name, new TypeUniquenessValidator("Organization"));
}
[...]
}
The structr REST servlet would understand GET, POST, PUT and DELETE
requests to those two new resources.
F.e. a POST to /software '{"name":"Software
X","version":"1.0","organization":"Organization Y"}' would internally
create two nodes:
A node of type "Software" with the properties "name", "version" and
"organization"
A node of type "Organization" with the properties "name" and "software"
GET /software would return a JSON object like this:
{
"query_time": "0.002025186",
"result_count": 1,
"result": [
{
"id": "c6c098b9517f4b61b9e490aa93029192",
"type": "Software",
"name": "Software X",
"version": "1.0",
"organization": "Organization Y"
}
]
}
And a GET /organizations would return:
{
"query_time": "0.002047697",
"result_count": 1,
"result": [
{
"id": "a53d59240ba64f2584b7aeda348eb0d3",
"type": "Organization",
"name": "Organization Y",
"softwares" : [
{
"id": "c6c098b9517f4b61b9e490aa93029192",
"type": "Software",
"name": "Software X",
"version": "1.0",
"organization": "Organization Y"
}
]
}
]
}
The uniqueness of Software objects would be ensured in the onCreation
method above, and it's basically a single index query using the
integrated lucene, searching for a node of type "Software" with the
given properties. If the uniqueness test fails, the transaction will not
be comitted, so no object created (no Software, no Organization).
The trick is, that the getter (and setter) methods of our entity classes
respect the property type defintions, e.g. local or related,
cardinality, uniqueness etc..
Best regards
Axel