跳到主要内容

唯一 ID 的处理与分配

DeepSeek V3 中英对照 Handling and provisioning of unique IDs

使用内部 Neo4j id

为你的领域类赋予唯一标识符的最简单方法是在 StringLong 类型的字段上结合使用 @Id@GeneratedValue(优选对象类型而非基本类型 long,因为字面量 null 是更好的指示符,用于判断实例是否为新创建的):

示例 1. 带有内部 Neo4j id 的 Mutable MovieEntity

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue
private Long id;

private String name;

public MovieEntity(String name) {
this.name = name;
}
}
java

你不需要为字段提供 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);
}
}
}
java
  • 不可变的最终 id 字段,表示生成的值

  • 公共构造函数,供应用程序和 Spring Data 使用

  • 内部使用的构造函数

  • 这是一个所谓的 wither 方法,用于 id 属性。它会创建一个新实体并相应地设置字段,而不修改原始实体,从而使其不可变。

如果你想拥有 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;
}
java

我们需要分别讨论两个方面的优缺点:任务本身和 UUID 策略。通用唯一标识符(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();
}
}
java

另一个选择是提供一个额外的 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();
}
}
java
  • 使用你需要的精确查询或逻辑。

上面的生成器将被配置为如下的 bean 引用:

示例 6. 使用 Spring Bean 作为 ID 生成器的可变 MovieEntity

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue(generatorRef = "myIdGenerator")
private String id;

private String name;
}
java

使用业务键

我们在完整示例的 MovieEntityPersonEntity 中使用了业务键。人员的名称在构造时由您的应用程序以及通过 Spring Data 加载时分配。

只有在找到一个稳定、唯一的业务键时,这才可能实现,但这样做可以创建出优秀的不可变领域对象。

  • 优点:使用业务或自然键作为主键是自然的。所涉及的实体被明确标识,并且在进一步建模领域时,大多数情况下感觉是合适的。

  • 缺点:一旦你发现你选择的键并不像你想象的那么稳定,业务键作为主键将难以更新。通常情况下,即使承诺不会改变,它也可能发生变化。除此之外,找到一个真正唯一标识某个事物的标识符是很困难的。

请记住,在 Spring Data Neo4j 处理域实体之前,始终会为其设置一个业务键。这意味着除非提供了一个 @Version 字段,否则它无法确定实体是否是新的(它总是假定实体是新的)。