Handling and provisioning of unique IDs
Using the internal Neo4j id
The easiest way to give your domain classes a unique identifier is the combination of @Id
and @GeneratedValue
on a field of type String
or Long
(preferable the object, not the scalar long
, as literal null
is the better indicator whether an instance is new or not):
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String name;
public MovieEntity(String name) {
this.name = name;
}
}
You don’t need to provide a setter for the field, SDN will use reflection to assign the field, but use a setter if there is one. If you want to create an immutable entity with an internally generated id, you have to provide a wither.
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private final Long id; (1)
private String name;
public MovieEntity(String name) { (2)
this(null, name);
}
private MovieEntity(Long id, String name) { (3)
this.id = id;
this.name = name;
}
public MovieEntity withId(Long id) { (4)
if (this.id.equals(id)) {
return this;
} else {
return new MovieEntity(id, this.title);
}
}
}
1 | Immutable final id field indicating a generated value |
2 | Public constructor, used by the application and Spring Data |
3 | Internally used constructor |
4 | This is a so-called wither for the id -attribute.
It creates a new entity and set’s the field accordingly, without modifying the original entity, thus making it immutable. |
You either have to provide a setter for the id attribute or something like a wither, if you want to have
-
Advantages: It is pretty clear that the id attribute is the surrogate business key, it takes no further effort or configuration to use it.
-
Disadvantage: It is tied to Neo4js internal database id, which is not unique to our application entity only over a database lifetime.
-
Disadvantage: It takes more effort to create an immutable entity
Use externally provided surrogate keys
The @GeneratedValue
annotation can take a class implementing org.springframework.data.neo4j.core.schema.IdGenerator
as parameter.
SDN provides InternalIdGenerator
(the default) and UUIDStringGenerator
out of the box.
The latter generates new UUIDs for each entity and returns them as java.lang.String
.
An application entity using that would look like this:
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(UUIDStringGenerator.class)
private String id;
private String name;
}
We have to discuss two separate things regarding advantages and disadvantages. The assignment itself and the UUID-Strategy. A universally unique identifier is meant to be unique for practical purposes. To quote Wikipedia: “Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not duplicate one that has already been, or will be, created to identify something else.” Our strategy uses Java internal UUID mechanism, employing a cryptographically strong pseudo random number generator. In most cases that should work fine, but your mileage might vary.
That leaves the assignment itself:
-
Advantage: The application is in full control and can generate a unique key that is just unique enough for the purpose of the application. The generated value will be stable and there won’t be a need to change it later on.
-
Disadvantage: The generated strategy is applied on the application side of things. In those days most applications will be deployed in more than one instance to scale nicely. If your strategy is prone to generate duplicates then inserts will fail as the uniqueness property of the primary key will be violated. So while you don’t have to think about a unique business key in this scenario, you have to think more what to generate.
You have several options to roll out your own ID generator. One is a POJO implementing a generator:
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.util.StringUtils;
public class TestSequenceGenerator implements IdGenerator<String> {
private final AtomicInteger sequence = new AtomicInteger(0);
@Override
public String generateId(String primaryLabel, Object entity) {
return StringUtils.uncapitalize(primaryLabel) +
"-" + sequence.incrementAndGet();
}
}
Another option is to provide an additional Spring Bean like this:
@Component
class MyIdGenerator implements IdGenerator<String> {
private final Neo4jClient neo4jClient;
public MyIdGenerator(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public String generateId(String primaryLabel, Object entity) {
return neo4jClient.query("YOUR CYPHER QUERY FOR THE NEXT ID") (1)
.fetchAs(String.class).one().get();
}
}
1 | Use exactly the query or logic your need. |
The generator above would be configured as a bean reference like this:
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(generatorRef = "myIdGenerator")
private String id;
private String name;
}
Using a business key
We have been using a business key in the complete example’s MovieEntity
and PersonEntity
.
The name of the person is assigned at construction time, both by your application and while being loaded through Spring Data.
This is only possible, if you find a stable, unique business key, but makes great immutable domain objects.
-
Advantages: Using a business or natural key as primary key is natural. The entity in question is clearly identified, and it feels most of the time just right in the further modelling of your domain.
-
Disadvantages: Business keys as primary keys will be hard to update once you realise that the key you found is not as stable as you thought. Often it turns out that it can change, even when promised otherwise. Apart from that, finding identifier that are truly unique for a thing is hard.
Please keep in mind that a business key is always set on the domain entity before Spring Data Neo4j processes it.
This means that it cannot determine if the entity was new or not (it always assumes that the entity is new),
unless also a @Version
field is provided.