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