唯一标识符的处理与提供
使用内部 Neo4j ID
为领域类提供唯一标识符最简单的方法,是在 String 或 Long 类型的字段上组合使用 @Id 与 @GeneratedValue(优先使用对象类型,而非标量 long,因为字面量 null 能更好地指示一个实例是否为新建):
示例 1. 使用内部 Neo4j ID 的可变 MovieEntity
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String name;
public MovieEntity(String name) {
this.name = name;
}
}
您无需为该字段提供 setter;SDN 会使用反射来赋值,但如果存在 setter 也会使用它。如果您想创建一个具有内部生成 ID 的不可变实体,则必须提供一个所谓的 wither。
示例 2. 使用内部 Neo4j ID 的不可变 MovieEntity
@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);
}
}
}
表示生成式值的不可变 final id 字段
公共构造函数,供应用程序和 Spring Data 使用
内部使用的构造函数
这是所谓
id-attribute 的 wither。它会创建一个新实体并相应地设置该字段,而不修改原实体,从而使其不可变。
如果您想采用这种方式,则必须为 id 属性提供一个 setter,或类似 wither 的机制。
-
优点:id 属性作为代理业务键非常清晰,使用它无需额外的努力或配置。
-
缺点:它与 Neo4j 的内部数据库 ID 绑定,该 ID 仅在数据库生命周期内对我们的应用程序实体唯一。
-
缺点:创建不可变实体需要更多工作。
使用外部提供的代理键
@GeneratedValue 注解可以接受一个实现了 org.springframework.data.neo4j.core.schema.IdGenerator 的类作为参数。SDN 开箱即用地提供了 InternalIdGenerator(默认)和 UUIDStringGenerator。后者为每个实体生成新的 UUID,并以 java.lang.String 形式返回。使用它的应用程序实体如下所示:
示例 3. 使用外部生成代理键的可变 MovieEntity
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(UUIDStringGenerator.class)
private String id;
private String name;
}
关于优缺点,我们必须讨论两个相互独立的方面:赋值本身和 UUID 策略。通用唯一标识符旨在为实际用途而唯一。引用维基百科的说法:“因此,任何人都可以创建一个 UUID,并以几乎确定的把握用它来标识某事物,确信该标识符不会与已经或将来为标识其他事物而创建的标识符重复。”我们的策略使用 Java 内部的 UUID 机制,采用密码学上强的伪随机数生成器。在大多数情况下这应该能正常工作,但具体情况可能有所不同。
剩下的就是赋值本身:
-
优点:应用程序完全可控,可以生成一个对应用程序用途而言足够唯一的键。生成的值是稳定的,之后无需再更改它。
-
缺点:生成策略应用在应用程序这一侧。如今大多数应用程序都会部署在多个实例中以实现良好的扩展。如果您的策略容易生成重复值,那么插入操作将因违反主键的唯一性约束而失败。因此,虽然在这种场景下您不必考虑唯一业务键,但必须更多地考虑要生成什么。
您有多种方式推出自己的 ID 生成器。一种是编写一个实现了生成器接口的 POJO:
示例 4. 朴素的序列生成器
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();
}
}
另一种方式是提供一个额外的 Spring Bean,如下所示:
示例 5. 基于 Neo4jClient 的 ID 生成器
@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();
}
}
完全使用您需要的查询或逻辑。
上面的生成器将像这样配置为一个 bean 引用:
示例 6. 使用 Spring Bean 作为 ID 生成器的可变 MovieEntity
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(generatorRef = "myIdGenerator")
private String id;
private String name;
}
使用业务键
在完整示例的 MovieEntity 和 PersonEntity 中,我们一直使用业务键。person 的姓名在构造时即被赋值,无论是由您的应用程序赋值,还是在通过 Spring Data 加载时赋值。
只有在找到一个稳定、唯一的业务键时这才可行,但它能很好地构建不可变的领域对象。
-
优点:使用业务键或自然键作为主键是很自然的。所讨论的实体被清晰地标识出来,而且在进一步对领域建模时,多数时候感觉就是恰到好处。
-
缺点:一旦您意识到找到的键并不像原先认为的那样稳定,以业务键作为主键将很难更新。通常结果证明它是可以更改的,即使当初承诺并非如此。除此之外,找到一个对某事物真正唯一的标识符也很困难。
请记住,业务键总是在 Spring Data Neo4j 处理领域实体之前就已设置好。这意味着它无法判断该实体是否为新建(它总是假定实体是新建的),除非同时提供了 @Version 字段。