![Spring Data(数据)Neo4j(二)_xml](//dev-img.mos.moduyun.com/20231025/e13ce805-3f64-4cba-a8ac-3a7cbf48b69a.jpg)
9.5.1. Java 配置
使用 Java 配置类上的特定于存储的注释来定义存储库激活的配置。 有关 Spring 容器的基于 Java 的配置的介绍,请参阅Spring 参考文档中的 JavaConfig。@EnableJpaRepositories
启用 Spring 数据存储库的示例配置类似于以下内容:
例 35。基于注释的存储库配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的示例使用特定于 JPA 的注释,您将根据实际使用的存储模块对其进行更改。这同样适用于thebean的定义。请参阅涵盖特定于商店的配置的部分。EntityManagerFactory |
9.5.2.XML 配置
每个 Spring 数据模块都包含一个元素,该元素允许您定义 Spring 为您扫描的基本包,如以下示例所示:repositories
例 36。通过XML启用Spring 数据存储库
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories" />
</beans:beans>
在前面的示例中,指示 Spring 扫描其所有子包以查找接口扩展或其子接口之一。 对于找到的每个接口,基础结构都会注册特定于持久性技术,以创建处理查询方法调用的相应代理。 每个 Bean 都注册在从接口名称派生的 Bean 名称下,因此将注册一个接口。 嵌套存储库接口的 Bean 名称以其封闭类型名称为前缀。 基本包属性允许通配符,以便您可以定义扫描包的模式。com.acme.repositories
Repository
FactoryBean
UserRepository
userRepository
9.5.3. 使用过滤器
缺省情况下,基础结构选取扩展位于配置的基础包下的特定于持久性技术的子接口的每个接口,并为其创建一个 Bean 实例。 但是,您可能希望更精细地控制哪些接口为其创建了 Bean 实例。 为此,请在存储库声明中使用过滤器元素。 语义与 Spring 组件过滤器中的元素完全相同。 有关详细信息,请参阅这些元素的Spring 参考文档。Repository
例如,要从实例化中排除某些接口作为存储库 Bean,您可以使用以下配置:
例 37。使用过滤器
清单 18.爪哇岛
清单 19..XML
@Configuration
@EnableJpaRepositories(basePackages = "com.acme.repositories",
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },
excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的示例排除了以实例化结尾的所有接口,并包括以 结尾的接口。SomeRepository
SomeOtherRepository
9.5.4. 独立使用
您还可以在 Spring 容器之外使用存储库基础架构,例如,在 CDI 环境中。你的类路径中仍然需要一些 Spring 库,但通常,你也可以以编程方式设置存储库。提供存储库支持的 Spring 数据模块附带了您可以使用的特定于持久性技术的模块,如下所示:RepositoryFactory
例 38。存储库工厂的独立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
9.6. Spring 数据存储库的自定义实现
Spring Data 提供了各种选项来创建查询方法,只需很少的编码。 但是,当这些选项不符合您的需求时,您还可以为存储库方法提供自己的自定义实现。 本节介绍如何执行此操作。
9.6.1. 自定义单个仓库
要使用自定义功能丰富存储库,您必须首先定义片段接口和自定义功能的实现,如下所示:
例 39。自定义存储库功能的界面
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
例 40。实现自定义存储库功能
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
与片段接口对应的类名中最重要的部分是后缀。Impl |
实现本身不依赖于 Spring 数据,可以是常规的 Spring bean。 因此,您可以使用标准的依赖注入行为来注入对其他 Bean 的引用(例如 a)、参与方面等。JdbcTemplate
然后,您可以让存储库接口扩展片段接口,如下所示:
例 41。对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
使用存储库接口扩展片段接口结合了 CRUD 和自定义功能,并使其可供客户端使用。
Spring 数据存储库是通过使用形成存储库组合的片段来实现的。 片段是基本存储库、功能方面(如QueryDsl)和自定义接口及其实现。 每次向存储库界面添加接口时,都会通过添加片段来增强组合。 基本存储库和存储库方面实现由每个 Spring 数据模块提供。
以下示例显示了自定义接口及其实现:
例 42。片段及其实现
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface ContactRepository {
void someContactMethod(User user);
User anotherContactMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
以下示例显示了扩展的自定义存储库的接口:CrudRepository
例 43。对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
存储库可以由多个自定义实现组成,这些实现按其声明顺序导入。 自定义实现的优先级高于基本实现和存储库方面。 此排序允许您覆盖基本存储库和方面方法,并在两个片段提供相同的方法签名时解决歧义。 存储库片段不限于在单个存储库界面中使用。 多个存储库可以使用片段界面,允许您跨不同存储库重用自定义项。
以下示例显示了存储库片段及其实现:
例 44。片段覆盖save(…)
interface CustomizedSave<T> {
<S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
以下示例显示了使用上述存储库片段的存储库:
例 45。自定义存储库接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
配置
存储库基础结构尝试通过扫描找到存储库的包下的类来自动检测自定义实现片段。 这些类需要遵循附加默认后缀的命名约定。Impl
以下示例显示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库:
例 46。配置示例
清单 20.爪哇岛
清单 21..XML
@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
前面示例中的第一个配置尝试查找调用充当自定义存储库实现的类。 第二个示例尝试查找。com.acme.repository.CustomizedUserRepositoryImpl
com.acme.repository.CustomizedUserRepositoryMyPostfix
歧义的解决
如果在不同的包中找到具有匹配类名的多个实现,Spring Data 将使用 bean 名称来标识要使用的实现。
给定前面所示的以下两个自定义实现,将使用第一个实现。 它的 Bean 名称是,与片段接口 () 加上后缀的名称相匹配。CustomizedUserRepository
customizedUserRepositoryImpl
CustomizedUserRepository
Impl
例 47。解决不明确的实现
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
如果您注释接口,则 bean 名称 plusthen 与为存储库实现定义的名称匹配,并且使用它代替第一个。UserRepository
@Component("specialCustom")
Impl
com.acme.impl.two
手动接线
如果您的自定义实现仅使用基于注释的配置和自动连线,则上述方法效果很好,因为它被视为任何其他 Spring Bean。 如果您的实现片段 Bean 需要特殊连接,则可以声明 Bean 并根据上一节中描述的约定对其进行命名。 然后,基础结构按名称引用手动定义的 Bean 定义,而不是自己创建一个。 以下示例演示如何手动连接自定义实现:
例 48。自定义实现的手动接线
清单 22.爪哇岛
清单 23..XML
class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
9.6.2. 自定义基础仓库
当您想要自定义基本存储库行为以使所有存储库都受到影响时,上一节中描述的方法需要自定义每个存储库接口。 要改为更改所有存储库的行为,您可以创建一个实现来扩展特定于持久性技术的存储库基类。 然后,此类充当存储库代理的自定义基类,如以下示例所示:
例 49。自定义存储库基类
class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
|
该类需要具有特定于存储的存储库工厂实现使用的超类的构造函数。 如果存储库基类有多个构造函数,请覆盖采用存储特定基础结构对象(例如模板类)的构造函数。EntityInformation EntityManager |
最后一步是使 Spring 数据基础架构知道自定义的存储库基类。 在配置中,可以使用 来执行此操作,如以下示例所示:repositoryBaseClass
例 50。配置自定义存储库基类
清单 24.爪哇岛
清单 25..XML
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
9.7. 从聚合根发布事件
存储库管理的实体是聚合根。 在域驱动设计应用程序中,这些聚合根通常发布域事件。 Spring Data 提供了一个注释,称为您可以在聚合根的方法上使用该注释,以使该发布尽可能简单,如以下示例所示:@DomainEvents
例 51。从聚合根公开域事件
class AnAggregateRoot {
@DomainEvents
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication
void callbackMethod() {
// … potentially clean up domain events list
}
}
|
使用的方法可以返回单个事件实例或事件集合。 它绝不能接受任何论据。@DomainEvents |
|
发布所有事件后,我们有一个注释方法。 您可以使用它来潜在地清理要发布的事件列表(以及其他用途)。@AfterDomainEventPublication |
每次调用 Spring 数据存储库或方法之一时都会调用这些方法。save(…)
saveAll(…)
delete(…)
deleteAll(…)
9.8. 弹簧数据扩展
本节记录了一组 Spring 数据扩展,这些扩展允许在各种上下文中使用 Spring 数据。 目前,大多数集成都是针对Spring MVC的。
9.8.1. 查询扩展
Querydsl是一个框架,它支持通过其流畅的API构造静态类型的类似SQL的查询。
几个 Spring 数据模块提供与 Querydsl 的集成,如以下示例所示:QuerydslPredicateExecutor
例 52。QuerydslPredicateExecutor interface
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
// … more functionality omitted.
}
|
查找并返回与 匹配的单个实体。Predicate |
|
查找并返回与 匹配的所有实体。Predicate |
|
返回与 匹配的实体数。Predicate |
|
返回与实体匹配的实体是否存在。Predicate |
要使用 Querydsl 支持,请扩展存储库接口,如以下示例所示:QuerydslPredicateExecutor
例 53。存储库上的 querydsl 集成
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前面的示例允许您使用 Querydslinstances 编写类型安全的查询,如以下示例所示:Predicate
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
9.8.2. 网页支持
支持存储库编程模型的 Spring 数据模块附带了各种 Web 支持。 与Web相关的组件要求Spring MVC JAR位于类路径上。 其中一些甚至提供与Spring HATEOAS的集成。 通常,集成支持是通过在 JavaConfig 配置类中使用注释来启用的,如以下示例所示:@EnableSpringDataWebSupport
例 54。启用 Spring 数据网络支持
清单 26.爪哇岛
清单 27..XML
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
注释注册了一些组件。 我们将在本节后面讨论这些内容。 它还检测类路径上的Spring HATEOAS,并为其注册集成组件(如果存在)。@EnableSpringDataWebSupport
基本网络支持
在XML中启用Spring Data Web支持
上一节中显示的配置注册了一些基本组件:
使用类DomainClassConverter
该类允许您直接在Spring MVC控制器方法签名中使用域类型,这样您就不需要通过存储库手动查找实例,如以下示例所示:DomainClassConverter
例 55。在方法签名中使用域类型的Spring MVC控制器
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
该方法直接接收实例,无需进一步查找。 可以通过让Spring MVC首先将路径变量转换为域类的类型,并最终通过调用为域类型注册的存储库实例来访问实例来解决该实例。User
id
findById(…)
|
目前,存储库必须实现才有资格被发现进行转换。CrudRepository |
用于可分页和排序的处理程序方法参数解析器
上一节中显示的配置片段还注册了 aas 以及 的实例。 注册启用 sandas 有效控制器方法参数,如以下示例所示:PageableHandlerMethodArgumentResolver
SortHandlerMethodArgumentResolver
Pageable
Sort
例 56。使用可分页作为控制器方法参数
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
前面的方法签名导致Spring MVC尝试使用以下默认配置从请求参数派生实例:Pageable
表 1.为实例评估的请求参数Pageable
page |
要检索的页面。0 索引,默认为 0。 |
size |
要检索的页面的大小。默认值为 20。 |
sort |
应按格式排序的属性。默认排序方向为区分大小写的升序。如果要切换方向或区分大小写,请使用多个参数,例如。property,property(,ASC|DESC)(,IgnoreCase) sort ?sort=firstname&sort=lastname,asc&sort=city,ignorecase |
要定制此行为,请分别注册实现接口或接口的 Bean。 调用 Itsmethod,允许您更改设置,如以下示例所示:PageableHandlerMethodArgumentResolverCustomizer
SortHandlerMethodArgumentResolverCustomizer
customize()
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
如果设置现有属性不足以满足您的目的,请扩展启用 HATEOAS 的等效项,覆盖理论方法,并导入自定义配置文件,而不是使用注释。MethodArgumentResolver
SpringDataWebConfiguration
pageableResolver()
sortResolver()
@Enable
如果您需要从请求中解析多个实例(例如,对于多个表),则可以使用 Spring'sannotation 来区分彼此。 然后,请求参数必须带有前缀。 下面的示例演示生成的方法签名:Pageable
Sort
@Qualifier
${qualifier}_
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
您必须填充,等等。thing1_page
thing2_page
默认传递给方法等效于 a,但您可以通过对参数使用注释来自定义它。Pageable
PageRequest.of(0, 20)
@PageableDefault
Pageable
对可分页的超媒体支持
Spring HATEOAS 附带了一个表示模型类 (),它允许使用必要的元数据和链接来丰富实例的内容,让客户端轻松浏览页面。 ato ais 的转换是通过 Spring HATEOAS接口的实现完成的,称为。 下面的示例演示如何使用 aas 控制器方法参数:PagedResources
Page
Page
Page
PagedResources
ResourceAssembler
PagedResourcesAssembler
PagedResourcesAssembler
例 57。使用 PagedResourcesAssembler 作为控制器方法参数
@Controller
class PersonController {
@Autowired PersonRepository repository;
@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}
启用配置(如前面的示例所示)允许将 thebe 用作控制器方法参数。 呼叫它具有以下效果:PagedResourcesAssembler
toResources(…)
- 的内容成为实例的内容。
Page
PagedResources
- 对象被附加一个实例,并填充来自底层的信息。
PagedResources
PageMetadata
Page
PageRequest
- 附上可能获取链接,具体取决于页面的状态。 链接指向方法映射到的 URI。 添加到方法的分页参数与设置匹配,以确保以后可以解析链接。
PagedResources
prev
next
PageableHandlerMethodArgumentResolver
假设数据库中有 30 个实例。 您现在可以触发请求 () 并查看类似于以下内容的输出:Person
GET http://localhost:8080/persons
{ "links" : [ { "rel" : "next",
"href" : "http://localhost:8080/persons?page=1&size=20" }
],
"content" : [
… // 20 Person instances rendered here
],
"pageMetadata" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
汇编程序生成了正确的 URI,并且还选取了默认配置,以将参数解析为 afor 即将到来的请求。 这意味着,如果更改该配置,链接将自动遵循更改。 默认情况下,汇编程序指向在其中调用它的控制器方法,但您可以通过传递用作构建分页链接的基础的自定义来自定义该方法,这会重载该方法。Pageable
Link
PagedResourcesAssembler.toResource(…)
弹簧数据杰克逊模块
核心模块,以及一些特定于商店的模块,附带了一组杰克逊模块,用于Spring Data域使用的类似类型。
一旦启用了Web 支持并且可用,这些模块就会被导入。org.springframework.data.geo.Distance
org.springframework.data.geo.Point
com.fasterxml.jackson.databind.ObjectMapper
在初始化期间,就像 一样,由基础结构拾取,以便声明的 Jackson 可用。SpringDataJacksonModules
SpringDataJacksonConfiguration
com.fasterxml.jackson.databind.Module
ObjectMapper
以下域类型的数据绑定混合由通用基础结构注册。
org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
单个模块可以提供额外的。 有关更多详细信息,请参阅商店特定部分。SpringDataJacksonModules
|
网页数据绑定支持
您可以使用 Spring 数据投影(在第10 章中描述)通过使用 JSONPath 表达式(需要Jayway JsonPath)或XPath表达式(需要XmlBeam)来绑定传入的请求有效负载,如以下示例所示:
例 58。使用 JSONPath 或 XPath 表达式的 HTTP 有效负载绑定
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
您可以使用前面示例中所示的类型作为 Spring MVC 处理程序方法参数,也可以使用 on 的方法之一。 前面的方法声明将尝试在给定文档中的任何地方查找。 XML 查找在传入文档的顶层执行。 它的 JSON 变体尝试顶级优先,但如果前者不返回值,也会尝试嵌套在子文档中。 这样,可以轻松缓解源文档结构中的更改,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。ParameterizedTypeReference
RestTemplate
firstname
lastname
lastname
lastname
user
支持嵌套投影,如第 10 章中所述。 如果该方法返回复杂的非接口类型,则使用 Jacksonis 映射最终值。ObjectMapper
对于Spring MVC,一旦激活,就会自动注册必要的转换器,并且所需的依赖项在类路径上可用。 如需使用 ,请注册 (JSON) 或手动注册。@EnableSpringDataWebSupport
RestTemplate
ProjectingJackson2HttpMessageConverter
XmlBeamHttpMessageConverter
有关更多信息,请参阅规范的 Spring 数据示例存储库中的Web 投影示例。
查询网络支持
对于那些具有QueryDSL集成的存储,可以从查询字符串中包含的属性派生查询。Request
请考虑以下查询字符串:
?firstname=Dave&lastname=Matthews
给定前面示例中的对象,可以使用 将查询字符串解析为以下值,如下所示:User
QuerydslPredicateArgumentResolver
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
当在类路径上找到 Querydsl 时,将自动启用该功能。@EnableSpringDataWebSupport |
将 ato 添加到方法签名提供了一个即用型,您可以使用 来运行。@QuerydslPredicate
Predicate
QuerydslPredicateExecutor
类型信息通常从方法的返回类型中解析。 由于该信息不一定与域类型匹配,因此最好使用 的属性。root QuerydslPredicate |
下面的示例演示如何在方法中使用签名:@QuerydslPredicate
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
将查询字符串参数解析为匹配对象。Predicate User |
默认绑定如下所示:
-
Object
在简单属性上。eq
-
Object
在集合上像属性一样。contains
-
Collection
在简单属性上。in
您可以通过属性 ofor 自定义这些绑定,方法是使用 Java 8 并将方法添加到存储库接口,如下所示:bindings
@QuerydslPredicate
default methods
QuerydslBinderCustomizer
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>,
QuerydslBinderCustomizer<QUser> {
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value))
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
bindings.excluding(user.password);
}
}
QuerydslPredicateExecutor 提供对特定查找器方法的访问。Predicate |
QuerydslBinderCustomizer 在存储库界面上定义的自动拾取和快捷方式。@QuerydslPredicate(bindings=…) |
将属性的绑定定义为简单绑定。username contains |
将属性的默认绑定定义为不区分大小写的匹配项。String contains |
从解析中排除该属性。password Predicate |
您可以在从存储库或应用特定绑定之前注册持有默认 Querydsl 绑定的 abean。QuerydslBinderCustomizerDefaults @QuerydslPredicate |
9.8.3. 仓库填充器
如果您使用 Spring JDBC 模块,您可能熟悉对填充 SQL 脚本的支持。 类似的抽象在存储库级别可用,尽管它不使用 SQL 作为数据定义语言,因为它必须独立于存储。 因此,填充器支持XML(通过Spring的OXM抽象)和JSON(通过Jackson)来定义用于填充存储库的数据。DataSource
假设您有一个包含以下内容的文件:data.json
例 59。在 JSON 中定义的数据
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
您可以使用 Spring 数据共享中提供的存储库命名空间的填充器元素来填充存储库。 若要将上述数据填充到 ur,请声明类似于以下内容的填充器:PersonRepository
例 60。声明杰克逊存储库填充器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans>
前面的声明会导致 Jackson 读取和反序列化该文件。data.json
ObjectMapper
JSON 对象解组的类型是通过检查 JSON 文档的属性来确定的。 基础结构最终会选择适当的存储库来处理反序列化的对象。_class
要改用 XML 来定义存储库应填充的数据,您可以使用 theelement。 您可以将其配置为使用 Spring OXM 中可用的 XML 编组选项之一。有关详细信息,请参阅Spring 参考文档。 以下示例显示如何使用 JAXB 取消编组存储库填充器:unmarshaller-populator
例 61。声明解组存储库填充器(使用 JAXB)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>
9.9. 示例查询
9.9.1. 简介
本章介绍按示例查询并说明如何使用它。
按示例查询 (QBE) 是一种用户友好的查询技术,具有简单的界面。 它允许创建动态查询,并且不需要您编写包含字段名称的查询。 事实上,按示例查询根本不要求您使用特定于存储的查询语言编写查询。
9.9.2. 用法
按示例查询 API 由四个部分组成:
- 探测器:具有填充字段的域对象的实际示例。
-
ExampleMatcher
:包含有关如何匹配特定字段的详细信息。 它可以在多个示例中重复使用。ExampleMatcher
-
Example
:由探头和探头组成。 它用于创建查询。Example
ExampleMatcher
-
FetchableFluentQuery
:提供流畅的 API,允许进一步自定义从 . 使用流畅的 API 可以为查询指定排序投影和结果处理。FetchableFluentQuery
Example
按示例查询非常适合多种用例:
- 使用一组静态或动态约束查询数据存储。
- 频繁重构域对象,无需担心中断现有查询。
- 独立于基础数据存储 API 工作。
按示例查询也有几个限制:
- 不支持嵌套或分组属性约束,例如。
firstname = ?0 or (firstname = ?1 and lastname = ?2)
- 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。
在开始使用按示例查询之前,您需要有一个域对象。 首先,请为存储库创建一个接口,如以下示例所示:
例 62。示例人员对象
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前面的示例显示了一个简单的域对象。 您可以使用它来创建. 默认情况下,将忽略具有值的字段,并使用特定于存储的默认值匹配字符串。Example
null
将属性包含在“按示例查询”条件中基于可空性。 始终包含使用基元类型 (,, ...) 的属性,除非ExampleMatcher忽略属性路径。int double |
可以使用工厂方法或使用ExampleMatcher.is 不可变来构建示例。 下面的清单显示了一个简单的示例:of
Example
例 63。简单示例
Person person = new Person();
person.setFirstname("Dave");
Example<Person> example = Example.of(person);
创建域对象的新实例。 |
设置要查询的属性。 |
创建。Example |
您可以使用存储库运行示例查询。 为此,请让您的存储库界面扩展。 以下清单显示了界面的摘录:QueryByExampleExecutor<T>
QueryByExampleExecutor
例 64.这QueryByExampleExecutor
public interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
9.9.3. 匹配器示例
示例不限于默认设置。 您可以使用 指定自己的字符串匹配、null 处理和特定于属性的设置的默认值,如以下示例所示:ExampleMatcher
例 65。具有自定义匹配的示例匹配器
Person person = new Person();
person.setFirstname("Dave");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("lastname")
.withIncludeNullValues()
.withStringMatcher(StringMatcher.ENDING);
Example<Person> example = Example.of(person, matcher);
创建域对象的新实例。 |
设置属性。 |
创建 anto 期望所有值都匹配。 即使没有进一步的配置,它也可以在此阶段使用。ExampleMatcher |
构造一个新忽略属性路径。ExampleMatcher lastname |
构造一个新以忽略属性路径并包含空值。ExampleMatcher lastname |
构造一个 newto 忽略属性路径,以包含 null 值,并执行后缀字符串匹配。ExampleMatcher lastname |
创建一个基于域对象和配置的 new。Example ExampleMatcher |
默认情况下,期望探测器上设置的所有值都匹配。 如果要获取与隐式定义的任何谓词匹配的结果,请使用。ExampleMatcher
ExampleMatcher.matchingAny()
您可以为单个属性指定行为(例如“名字”和“姓氏”,或者对于嵌套属性,可以指定“address.city”)。 您可以使用匹配选项和区分大小写来调整它,如以下示例所示:
例 66。配置匹配器选项
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
配置匹配器选项的另一种方法是使用 lambda(在 Java 8 中引入)。 此方法创建一个回调,要求实现者修改匹配器。 您无需返回匹配器,因为配置选项保存在匹配器实例中。 以下示例显示了使用 lambda 的匹配器:
例 67。使用 lambda 配置匹配器选项
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
使用配置的合并视图创建的查询。 可以在级别设置默认匹配设置,而可以将单个设置应用于特定属性路径。 设置的设置由属性路径设置继承,除非显式定义它们。 属性修补程序上的设置具有比默认设置更高的优先级。 下表描述了各种设置的范围:Example
ExampleMatcher
ExampleMatcher
ExampleMatcher
表 2.设置的范围ExampleMatcher
设置 |
范围 |
空处理 |
ExampleMatcher |
字符串匹配 |
ExampleMatcher 和属性路径 |
忽略属性 |
属性路径 |
区分大小写 |
ExampleMatcher 和属性路径 |
价值转型 |
属性路径 |
9.9.4. 流畅的接口
QueryByExampleExecutor
提供了另一种方法,到目前为止我们没有提到: 与其他方法一样,它执行从 . 但是,使用第二个参数,您可以控制该执行的某些方面,否则无法动态控制。 为此,可以在第二个参数中调用各种方法。用于指定结果的排序。用于指定要将结果转换为的类型。限制查询的属性。,,,,,,,,并定义获得的结果类型以及当可用结果数超过预期数时查询的行为方式。<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)
Example
FetchableFluentQuery
sortBy
as
project
first
firstValue
one
oneValue
all
page
stream
count
exists
例 68。使用流畅的 API 获取可能许多结果中的最后一个,按姓氏排序。
Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);
9.10. 弹簧数据 Neo4j 扩展
9.10.1. Spring Data Neo4j 存储库的可用扩展
Spring Data Neo4j提供了一些可以添加到存储库中的扩展或“mixins”。什么是混合?根据维基百科,mixins是一种语言概念,允许程序员注入一些代码 成一个类。Mixin 编程是一种软件开发风格,其中功能单元是在类中创建的 然后与其他类混在一起。
Java在语言级别不支持这个概念,但我们确实通过几个接口和一个运行时来模拟它。 这将为其添加适当的实现和拦截器。
默认添加的混音分别是。这些接口是 在第9.9 节中详细解释。QueryByExampleExecutor
ReactiveQueryByExampleExecutor
提供的其他混音包括:
-
QuerydslPredicateExecutor
-
CypherdslConditionExecutor
-
CypherdslStatementExecutor
-
ReactiveQuerydslPredicateExecutor
-
ReactiveCypherdslConditionExecutor
-
ReactiveCypherdslStatementExecutor
向生成的查询添加动态条件
两者提供相同的概念:SDN生成查询,你 提供将添加的“谓词”(查询DSL)或“条件”(密码DSL)。我们推荐Cypher DSL,因为这是 SDN 6 本机使用的内容。您甚至可能要考虑使用生成 适合您的静态元模型。QuerydslPredicateExecutor
CypherdslConditionExecutor
这是怎么回事?如上所述声明存储库,并添加以下接口之一:
interface QueryDSLPersonRepository extends
Neo4jRepository<Person, Long>,
QuerydslPredicateExecutor<Person> {
}
或
interface PersonRepository extends
Neo4jRepository<Person, Long>,
CypherdslConditionExecutor<Person> {
}
密码DSL条件执行器显示了示例用法:
Node person = Cypher.node("Person").named("person");
Property firstName = person.property("firstName");
Property lastName = person.property("lastName");
assertThat(
repository.findAll(
firstName.eq(Cypher.anonParameter("Helge"))
.or(lastName.eq(Cypher.parameter("someName", "B."))),
lastName.descending()
))
.extracting(Person::getFirstName)
.containsExactly("Helge", "Bela");
定义命名对象,以查询的根为目标Node |
从中派生一些属性 |
创建条件。匿名参数用于名字,命名参数用于 姓氏。这是您在这些片段中定义参数的方式,也是与 Query-DSL 相比的优势之一。 混合不能做到这一点。 文字可以用。or Cypher.literalOf |
从其中一个属性定义SortItem |
代码看起来与Query-DSL mixin非常相似。Query-DSL mixin 的原因可能是对 API 的熟悉程度和 它也适用于其他商店。反对它的原因是您需要在类路径上附加一个库, 它缺少对遍历关系的支持,以及上述事实,即它不支持其 谓词(从技术上讲确实如此,但没有 API 方法将它们实际传递给正在执行的查询)。
对实体和投影使用(动态)Cypher-DSL 语句
添加相应的 mixin 与使用条件执行器没有什么不同:
interface PersonRepository extends
Neo4jRepository<Person, Long>,
CypherdslStatementExecutor<Person> {
}
请使用扩展时。ReactiveCypherdslStatementExecutor
ReactiveNeo4jRepository
它带有几个过载。他们都采用密码-DSL 语句分别将其定义为第一参数的持续定义,并且在投影方法的情况下,为类型。CypherdslStatementExecutor
findOne
findAll
如果查询需要参数,则必须通过 Cypher-DSL 本身定义它们,并且还由它填充,如以下清单所示:
static Statement whoHasFirstNameWithAddress(String name) {
Node p = Cypher.node("Person").named("p");
Node a = Cypher.anyNode("a");
Relationship r = p.relationshipTo(a, "LIVES_AT");
return Cypher.match(r)
.where(p.property("firstName").isEqualTo(Cypher.anonParameter(name)))
.returning(
p.getRequiredSymbolicName(),
Functions.collect(r),
Functions.collect(a)
)
.build();
}
@Test
void fineOneShouldWork(@Autowired PersonRepository repository) {
Optional<Person> result = repository.findOne(whoHasFirstNameWithAddress("Helge"));
assertThat(result).hasValueSatisfying(namesOnly -> {
assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
assertThat(namesOnly.getAddress()).extracting(Person.Address::getCity)
.isEqualTo("Mülheim an der Ruhr");
});
}
@Test
void fineOneProjectedShouldWork(@Autowired PersonRepository repository) {
Optional<NamesOnly> result = repository.findOne(
whoHasFirstNameWithAddress("Helge"),
NamesOnly.class
);
assertThat(result).hasValueSatisfying(namesOnly -> {
assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
assertThat(namesOnly.getFullName()).isEqualTo("Helge Schneider");
});
}
|
动态查询在帮助程序方法中以类型安全的方式生成
|
|
我们已经在这里看到了这一点,我们还定义了一些保存模型的变量。 |
|
我们定义一个匿名参数,由实际值填充,该参数被传递给方法name |
|
从帮助程序方法返回的语句用于查找实体
|
|
或投影。
|
方法的工作方式类似。 命令式 Cypher-DSL 语句执行器还提供返回分页结果的重载。findAll
10. 预测
Spring 数据查询方法通常返回由存储库管理的聚合根的一个或多个实例。 但是,有时可能需要基于这些类型的某些属性创建投影。 Spring 数据允许对专用返回类型进行建模,以更有选择性地检索托管聚合的部分视图。
假设存储库和聚合根类型,如以下示例所示:
例 69。示例聚合和存储库
class Person {
@Id UUID id;
String firstname, lastname;
Address address;
static class Address {
String zipCode, city, street;
}
}
interface PersonRepository extends Repository<Person, UUID> {
Collection<Person> findByLastname(String lastname);
}
现在假设我们只想检索人员的姓名属性。 Spring Data提供了什么手段来实现这一目标?本章的其余部分将回答这个问题。
10.1. 基于接口的投影
将查询结果限制为仅 name 属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示:
例 70。用于检索属性子集的投影接口
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里重要的一点是,此处定义的属性与聚合根中的属性完全匹配。 这样做可以添加查询方法,如下所示:
例 71。使用基于接口的投影和查询方法的存储库
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎在运行时为返回的每个元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。
在 yourThat 中声明方法会覆盖基方法(例如,声明在、特定于存储的存储库接口或 the)会导致对基方法的调用,而不管声明的返回类型如何。请确保使用兼容的返回类型,因为基方法不能用于投影。某些存储模块支持注释,以将重写的基方法转换为查询方法,然后可用于返回投影。Repository CrudRepository Simple…Repository @Query |
投影可以递归使用。如果还希望包含某些信息,请为其创建一个投影接口,并从声明中返回该接口,如以下示例所示:Address
getAddress()
例 72。用于检索属性子集的投影接口
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在方法调用时,将获取目标实例的属性并依次包装到投影代理中。address
![Spring Data(数据)Neo4j(二)_自定义_02](//dev-img.mos.moduyun.com/20231025/42614b61-d93f-4bfa-91d6-d50c528269d2.png)
10.1.1. 闭合投影
其访问器方法都与目标聚合的属性匹配的投影接口被视为封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:
例 73。封闭投影
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果您使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。 有关这方面的更多详细信息,请参阅参考文档中特定于模块的部分。
10.1.2. 开放投影
投影接口中的访问器方法还可用于通过注释来计算新值,如以下示例所示:@Value
例 74。开放式投影
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
支持投影的聚合根在变量中可用。 投影接口使用是开放式投影。 在这种情况下,Spring 数据无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。target
@Value
使用的表达式不应该太复杂 — 您希望避免对变量进行编程。 对于非常简单的表达式,一种选择可能是采用默认方法(Java 8 中引入),如以下示例所示:@Value
String
例 75。使用自定义逻辑的默认方法的投影接口
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
此方法要求您能够完全基于投影接口上公开的其他访问器方法实现逻辑。 第二个更灵活的选项是在 Spring Bean 中实现自定义逻辑,然后从 SpEL 表达式中调用该逻辑,如以下示例所示:
例 76。示例人员对象
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
请注意 SpEL 表达式如何引用和调用该方法,并将投影目标作为方法参数转发。 SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。 方法参数可通过名为的数组获得。下面的示例演示如何从数组中获取方法参数:myBean
getFullName(…)
Object
args
args
例 77。示例人员对象
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
同样,对于更复杂的表达式,您应该使用 Spring Bean 并让表达式调用方法,如前所述。
10.1.3. 可为空的包装器
投影接口中的 Getter 可以使用可为空的包装器来提高空安全性。目前支持的包装器类型包括:
-
java.util.Optional
-
com.google.common.base.Optional
-
scala.Option
-
io.vavr.control.Option
例 78。使用可为空包装器的投影接口
interface NamesOnly {
Optional<String> getFirstname();
}
如果基础投影值不是,则使用包装器的当前表示形式返回值。 如果支持值为 ,则 getter 方法返回所用包装类型的空表示形式。null
null
10.2. 基于类的投影 (DTO)
定义投影的另一种方法是使用值类型 DTO(数据传输对象),用于保存应检索的字段的属性。 这些 DTO 类型的使用方式与投影接口的使用方式完全相同,只是不会发生代理,并且不能应用嵌套投影。
如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。
以下示例显示了一个投影 DTO:
例 79。一个突出的DTO
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
}
避免投影 DTO 的样板代码
你可以通过使用ProjectLombok来大大简化DTO的代码,它提供了anannotation(不要与前面的接口示例中所示的Spring'sannotation混淆)。 如果您使用龙目岛项目的注释,前面显示的示例 DTO 将变为以下内容:@Value @Value @Value
@Value class NamesOnly { String firstname, lastname; }
字段是默认的,该类公开一个构造函数,该构造函数采用所有字段并自动获取实现的 sand方法。private final equals(…) hashCode()
|
10.3. 动态投影
到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。 但是,您可能希望选择要在调用时使用的类型(这使其成为动态的)。 若要应用动态投影,请使用查询方法,如以下示例所示:
例 80。使用动态投影参数的存储库
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
这样,该方法可用于按原样获取聚合或应用投影,如以下示例所示:
例 81。使用具有动态投影的存储库
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
检查类型的查询参数是否符合动态投影参数的条件。 如果查询的实际返回类型等于参数的泛型参数类型,则匹配参数不可用于查询或 SpEL 表达式。 如果要使用 aparameter 作为查询参数,请确保使用其他泛型参数,例如。Class Class Class Class Class<?> |
10.4. 一般说明
如上所述,投影有两种形式:基于接口和基于 DTO 的投影。 在Spring Data Neo4j中,这两种类型的投影都直接影响属性和关系的转移 通过电线。 因此,这两种方法都可以减少数据库上的负载,以防您处理包含以下内容的节点和实体 许多属性,这些属性在应用程序中的所有使用方案中可能都不需要。
对于基于接口和DTO的投影,Spring Data Neo4j将使用存储库的域类型来构建 查询。将考虑可能更改查询的所有属性上的所有注释。 域类型是通过存储库声明定义的类型 (给定一个类似于域类型的声明)。interface TestRepository extends CrudRepository<TestEntity, Long>
TestEntity
基于接口的投影将始终是基础域类型的动态代理。定义的访问器的名称 在这样的接口上(如)必须解析为属性(这里:)存在于投影实体上。 这些属性是否在域类型上具有访问器并不相关,只要可以通过 通用的 Spring 数据基础架构。后者已经得到保证,因为域类型不会是 第一名。getName
name
基于 DTO 的投影在与自定义查询一起使用时更加灵活。虽然标准查询派生自 原始域类型,因此只能使用其中定义的属性和关系,自定义查询 可以添加其他属性。
规则如下:首先,域类型的属性用于填充 DTO。如果DTO声明 其他属性 - 通过访问器或字段 - Spring Data Neo4j 在结果记录中查找匹配的属性。 属性必须按名称完全匹配,并且可以是简单类型(如 中定义) 或已知的持久实体。支持这些集合,但不支持地图。org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes
10.5. 多级投影
Spring Data Neo4j还支持多级投影。
清单 28.多级投影示例
interface ProjectionWithNestedProjection {
String getName();
List<Subprojection1> getLevel1();
interface Subprojection1 {
String getName();
List<Subprojection2> getLevel2();
}
interface Subprojection2 {
String getName();
}
}
即使可以对循环投影进行建模或指向将创建循环的实体, 投影逻辑不会遵循这些周期,而只会创建无周期查询。
多级投影绑定到它们应该投影的实体。在这种情况下属于实体类别,如果应用投影,则需要得到尊重。RelationshipProperties
10.6. 投影的数据操作
如果已将投影作为 DTO 获取,则可以修改其值。 但是,如果您使用的是基于接口的投影,则不能只更新界面。 可以使用的典型模式是在域实体类中提供一个方法,该方法使用接口并使用从接口复制的值创建域实体。 这样,您就可以更新实体,并使用投影蓝图/掩码再次保留它,如下一节所述。
10.7. 投影的持久性
类似于通过投影检索数据,它们也可以用作持久性的蓝图。 它提供了一个流畅的 API,用于将这些投影应用于保存操作。Neo4jTemplate
您可以为给定域类保存投影
清单 29.保存给定域类的投影
Projection projection = neo4jTemplate.save(DomainClass.class).one(projectionValue);
或者,您可以保存域对象,但仅遵循投影中定义的字段。
清单 30.使用给定的投影蓝图保存域对象
Projection projection = neo4jTemplate.saveAs(domainObject, Projection.class);
在这两种情况下,也可用于基于集合的操作,只有字段和关系 投影中定义的将得到更新。
防止删除数据(例如删除关系), 应始终至少加载以后应保留的所有数据。 |
10.8. 完整示例
给定以下实体、预测和相应的存储库:
清单 31.一个简单的实体
@Node
class TestEntity {
@Id @GeneratedValue private Long id;
private String name;
@Property("a_property")
private String aProperty;
}
清单 32.派生实体,继承自TestEntity
@Node
class ExtendedTestEntity extends TestEntity {
private String otherAttribute;
}
清单 33.接口投影TestEntity
interface TestEntityInterfaceProjection {
String getName();
}
清单 34.DTO投影,包括一个附加属性TestEntity
class TestEntityDTOProjection {
private String name;
private Long numberOfRelations;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getNumberOfRelations() {
return numberOfRelations;
}
public void setNumberOfRelations(Long numberOfRelations) {
this.numberOfRelations = numberOfRelations;
}
}
下面显示了一个存储库,它将按照列表的说明运行。TestEntity
清单 35.一个存储库TestEntity
interface TestRepository extends CrudRepository<TestEntity, Long> {
List<TestEntity> findAll();
List<ExtendedTestEntity> findAllExtendedEntities();
List<TestEntityInterfaceProjection> findAllInterfaceProjections();
List<TestEntityDTOProjection> findAllDTOProjections();
@Query("MATCH (t:TestEntity) - [r:RELATED_TO] -> () RETURN t, COUNT(r) AS numberOfRelations")
List<TestEntityDTOProjection> findAllDTOProjectionsWithCustomQuery();
}
存储库的域类型为TestEntity |
返回一个或多个的方法将只返回它的实例,因为它与域类型匹配TestEntity |
返回一个或多个扩展域类型的类实例的方法将只返回实例 的扩展类。所讨论方法的域类型将是扩展类,它 仍然满足存储库本身的域类型 |
此方法返回接口投影,因此该方法的返回类型不同 从存储库的域类型。接口只能访问域类型中定义的属性 |
此方法返回 DTO 投影。执行它将导致 SDN 发出警告,因为 DTO 定义为附加属性,该属性不在域类型的协定中。 带的属性将在查询中正确转换。 如上所述,返回类型与存储库的域类型不同。numberOfRelations aProperty TestEntity a_property |
此方法还返回 DTO 投影。但是,不会发出警告,因为查询包含管接头 投影中定义的附加属性的值。 |
虽然上面列表中的存储库使用具体的返回类型 定义投影,另一种变体是使用动态投影,如 Spring Data Neo4j 与其他 Spring Data Projects 共享的部分文档。动态投影可以是 应用于封闭接口投影和开放接口投影以及基于类的 DTO 投影:
动态投影的关键是将所需的投影类型指定为查询方法的最后一个参数 在这样的存储库中:。这是一个声明 可以添加到上述内容中,并允许通过同一方法检索不同的投影,而无需 以在多个方法上重复可能的注释。<T> Collection<T> findByName(String name, Class<T> type) TestRepository @Query |
11. 测试
11.1. 没有弹簧启动
我们在自己的集成测试中使用抽象基类进行大量配置。它们可以像这样使用:
清单 36.一种可能的测试设置,无需 Spring 引导
@ExtendWith(SpringExtension.class)
class YourIntegrationTest {
@Test
void thingsShouldWork(@Autowired Neo4jTemplate neo4jTemplate) {
// Add your test
}
@Configuration
@EnableNeo4jRepositories(considerNestedRepositories = true)
@EnableTransactionManagement
static class Config extends AbstractNeo4jConfig {
@Bean
public Driver driver() {
return GraphDatabase.driver("bolt://yourtestserver:7687", AuthTokens.none());
}
}
}
- 在这里,应提供与测试服务器或容器的连接。
为反应性测试提供了类似的类。
11.2. 使用弹簧启动和@DataNeo4jTest
Spring Boot 提供通过。 后者带来了包含注释和 所需的基础结构代码。@DataNeo4jTest
org.springframework.boot:spring-boot-starter-test
org.springframework.boot:spring-boot-test-autoconfigure
清单 37.在 Maven 构建中包含 Spring 启动启动测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
清单 38.在 Gradle 构建中包含 Spring 启动启动测试
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
@DataNeo4jTest
是一个 Spring 启动测试片。 测试切片为使用 Neo4j 进行测试提供了所有必要的基础设施:事务管理器、客户端、模板和声明的存储库,在其命令式或反应式变体中, 取决于是否存在反应式依赖关系。 测试切片已经包含,因此它与JUnit 5(JUnit Jupiter)一起自动运行。@ExtendWith(SpringExtension.class)
@DataNeo4jTest
默认情况下提供命令式和反应式基础架构,并添加隐式 well.in 然而,Spring 测试总是意味着命令式事务,因为声明式事务需要 返回方法的类型,以确定是否需要命令式或反应式。@Transactional
@Transactional
PlatformTransactionManager
ReactiveTransactionManager
要断言反应式存储库或服务的正确事务行为,您需要将 a注入测试或将域逻辑包装在使用带注释的方法的服务中,公开返回类型,使之成为可能 以便基础结构选择正确的事务管理器。TransactionalOperator
测试片不会引入嵌入式数据库或任何其他连接设置。 由您使用适当的连接。
我们建议使用以下两个选项之一:使用 Neo4j 测试容器模块或 Neo4j 测试工具。 虽然Testcontainers是一个已知的项目,其中包含许多不同服务的模块,但Neo4j测试工具却相当未知。 它是一个嵌入式实例,在测试存储过程时特别有用,如测试基于 Neo4j 的 Java 应用程序中所述。 但是,测试工具也可用于测试应用程序。 由于它会在与您的应用程序相同的 JVM 中打开一个数据库,因此性能和计时可能与您的生产设置不同。
为了您的方便,我们提供了三种可能的场景,Neo4j测试工具3.5和4.0以及测试容器Neo4j。 我们为 3.5 和 4.0 提供了不同的示例,因为测试工具在这些版本之间发生了变化。 此外,4.0 需要 JDK 11。
11.2.1.使用 Neo4j 测试工具 3.5@DataNeo4jTest
您需要以下依赖项来运行清单 40:
清单 39.Neo4j 3.5 测试工具依赖项
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>3.5.23</version>
<scope>test</scope>
</dependency>
Neo4j 3.5 企业版的依赖项在 适当的存储库配置。com.neo4j.test:neo4j-harness-enterprise
清单 40.使用 Neo4j 3.5 测试工具
@DataNeo4jTest
class MovieRepositoryTest {
private static ServerControls embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = TestServerBuilders.newInProcessBuilder()
.newServer();
}
@AfterAll
static void stopNeo4j() {
embeddedDatabaseServer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedDatabaseServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> null);
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
创建嵌入式 Neo4j 的入口点 |
这是一个 Spring 引导注释,允许动态注册 应用程序属性。我们覆盖相应的 Neo4j 设置。 |
完成所有测试后关闭 Neo4j。 |
11.2.2.使用 Neo4j 测试工具 4.x@DataNeo4jTest
您需要以下依赖项来运行清单 42:
清单 41.Neo4j 4.x 测试工具依赖项
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>{neo4j-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
Neo4j 4.x 企业版的依赖项可在 适当的存储库配置。com.neo4j.test:neo4j-harness-enterprise
清单 42.使用 Neo4j 4.x 测试工具
@DataNeo4jTest
class MovieRepositoryTest {
private static Neo4j embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.build();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedDatabaseServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> null);
}
@AfterAll
static void stopNeo4j() {
embeddedDatabaseServer.close();
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
创建嵌入式 Neo4j 的入口点 |
禁用不需要的 Neo4j HTTP 服务器 |
这是一个 Spring 引导注释,允许动态注册 应用程序属性。我们覆盖相应的 Neo4j 设置。 |
完成所有测试后关闭 Neo4j。 |
11.2.3.使用测试容器 Neo4j@DataNeo4jTest
当然,配置连接的原则与 Testcontainers 相同,如清单 43 所示。 您需要以下依赖项:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>1.14.3</version>
<scope>test</scope>
</dependency>
还有一个完整的测试:
清单 43.使用测试容器
@DataNeo4jTest
class MovieRepositoryTCTest {
private static Neo4jContainer<?> neo4jContainer;
@BeforeAll
static void initializeNeo4j() {
neo4jContainer = new Neo4jContainer<>()
.withAdminPassword("somePassword");
neo4jContainer.start();
}
@AfterAll
static void stopNeo4j() {
neo4jContainer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4jContainer::getAdminPassword);
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
11.2.4. 替代@DynamicPropertySource
在某些情况下,上述注释不适合您的用例。 其中之一可能是您希望 100% 控制驱动程序的初始化方式。 运行测试容器后,可以使用嵌套的静态配置类执行此操作,如下所示:
@TestConfiguration(proxyBeanMethods = false)
static class TestNeo4jConfig {
@Bean
Driver driver() {
return GraphDatabase.driver(
neo4jContainer.getBoltUrl(),
AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword())
);
}
}
如果要使用这些属性但不能使用 ,则可以使用初始值设定项:@DynamicPropertySource
清单 44.动态特性的替代注入
@ContextConfiguration(initializers = PriorToBoot226Test.Initializer.class)
@DataNeo4jTest
class PriorToBoot226Test {
private static Neo4jContainer<?> neo4jContainer;
@BeforeAll
static void initializeNeo4j() {
neo4jContainer = new Neo4jContainer<>()
.withAdminPassword("somePassword");
neo4jContainer.start();
}
@AfterAll
static void stopNeo4j() {
neo4jContainer.close();
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.neo4j.uri=" + neo4jContainer.getBoltUrl(),
"spring.neo4j.authentication.username=neo4j",
"spring.neo4j.authentication.password=" + neo4jContainer.getAdminPassword()
).applyTo(configurableApplicationContext.getEnvironment());
}
}
}
12. 审计
12.1. 基础知识
Spring Data提供了复杂的支持,可以透明地跟踪谁创建或更改了实体以及更改发生的时间。若要从该功能中受益,必须为实体类配备可以使用或通过实现接口来定义的审核元数据。 此外,必须通过注释配置或 XML 配置启用审核,以注册所需的基础结构组件。 有关配置示例,请参阅特定于商店的部分。
|
不需要仅跟踪创建和修改日期的应用程序会使其实体实现AuditorAware。
|
12.1.1. 基于注释的审计元数据
我们提供捕获创建或修改实体的用户以及何时发生更改的捕获。@CreatedBy
@LastModifiedBy
@CreatedDate
@LastModifiedDate
例 82。被审计的实体
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
}
如您所见,注释可以有选择地应用,具体取决于要捕获的信息。 指示在进行更改时捕获的注释可用于 JDK8 日期和时间类型,,,以及旧版 Javaand 的属性。long
Long
Date
Calendar
审核元数据不一定需要位于根级实体中,但可以添加到嵌入的实体中(取决于使用的实际存储),如下面的代码片段所示。
例 83。审核嵌入实体中的元数据
class Customer {
private AuditMetadata auditingMetadata;
// … further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}
12.1.2. 基于接口的审计元数据
如果您不想使用注释来定义审核元数据,则可以让您的域类实现接口。它公开所有审核属性的 setter 方法。Auditable
12.1.3. AuditorAware
如果使用任一 or,则审计基础结构需要以某种方式了解当前主体。为此,我们提供了 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义属性的类型。@CreatedBy
@LastModifiedBy
AuditorAware<T>
T
@CreatedBy
@LastModifiedBy
以下示例显示了使用 Spring 安全性对象的接口的实现:Authentication
例 84。基于弹簧安全性的实现AuditorAware
class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
该实现访问 Spring 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。Authentication
UserDetails
UserDetailsService
UserDetails
Authentication
12.1.4. ReactiveAuditorAware
使用响应式基础结构时,您可能希望利用上下文信息来提供信息。 我们提供 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义属性的类型。@CreatedBy
@LastModifiedBy
ReactiveAuditorAware<T>
T
@CreatedBy
@LastModifiedBy
以下示例显示了使用反应式 Spring 安全性对象的接口的实现:Authentication
例 85。基于弹簧安全性的实现ReactiveAuditorAware
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
该实现访问 Spring 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。Authentication
UserDetails
UserDetailsService
UserDetails
Authentication
常见问题
SDN与Neo4j-OGM有何关系?
Neo4j-OGM是一个对象图映射库,主要由Spring Data Neo4j的先前版本用作其后端,用于将节点和关系映射到域对象的繁重工作。 当前的SDN不需要也不支持Neo4j-OGM。 SDN专门使用Spring Data的映射上下文来扫描类和构建元模型。
虽然这将SDN固定在Spring生态系统中,但它有几个优点,其中包括CPU和内存使用方面的占用空间较小,尤其是Spring映射上下文的所有功能。
为什么我应该使用SDN而不是SDN + OGM
SDN具有SDN + OGM中不存在的几个功能,特别是
- 完全支持 Springs 反应式故事,包括反应式事务
- 完全支持按示例查询
- 完全支持完全不可变的实体
- 支持派生查找器方法的所有修饰符和变体,包括空间查询
SDN是否支持通过HTTP连接到Neo4j?
不。
SDN是否支持嵌入式Neo4j?
嵌入式Neo4j具有多个方面:
SDN 是否为您的应用程序提供嵌入式实例?
不。
SDN 是否直接与嵌入式实例交互?
不。 嵌入式数据库通常由 的实例表示,并且没有开箱即用的 Bolt 连接器。org.neo4j.graphdb.GraphDatabaseService
然而,SDN可以与Neo4j的测试工具一起工作,测试工具专门用于替代真实数据库。 对 Neo4j 3.5 和 4.x 测试工具的支持是通过驱动程序的 Spring Boot 启动器实现的。 看看相应的模块。org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure
可以使用哪种Neo4j Java驱动程序以及如何使用?
SDN6依赖于Neo4j Java驱动程序版本4.x。 每个SDN6版本都使用与当时可用的最新Neo4j兼容的Neo4j Java驱动程序版本 释放。 虽然Neo4j Java驱动程序的补丁版本通常是直接替换,但SDN6确保即使是次要版本。 可互换,因为它在必要时检查是否存在方法或接口更改。
因此,您可以将任何4.x Neo4j Java驱动程序与任何SDN 6.x版本一起使用。
带弹簧启动
如今,Spring 启动部署是最可能的基于 Spring Data 的应用程序的部署。请使用 Spring 启动依赖关系管理来更改驱动程序版本,如下所示:
清单 45.从 Maven 更改驱动程序版本 (pom.xml)
<properties>
<neo4j-java-driver.version>4.4.2</neo4j-java-driver.version>
</properties>
或
清单 46.从 Gradle (gradle.properties) 更改驱动程序版本
neo4j-java-driver.version = 4.4.2
不带弹簧启动
如果没有 Spring Boot,您只需手动声明依赖项。对于 Maven,我们建议使用这样的部分:<dependencyManagement />
清单 47.从Maven更改没有Spring Boot的驱动程序版本(pom.xml)
<dependencyManagement>
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>4.4.2</version>
</dependency>
</dependencyManagement>
Neo4j 4.0 支持多个数据库 - 如何使用它们?
可以静态配置数据库名称,也可以运行自己的数据库名称提供程序。 请记住,SDN 不会为您创建数据库。 您可以借助迁移工具或预先使用简单的脚本来执行此操作。
静态配置
配置要在 Spring 引导配置中使用的数据库名称,如下所示(相同的属性当然适用于 YML 或基于环境的配置,并应用 Spring 引导的约定):
spring.data.neo4j.database = yourDatabase
完成该配置后,由SDN存储库的所有实例(反应式和命令式)以及分别由数据库生成的所有查询都将针对数据库执行。ReactiveNeo4jTemplate
Neo4jTemplate
yourDatabase
动态配置
提供带有类型或的 bean 取决于您的 Spring 应用程序的类型。Neo4jDatabaseNameProvider
ReactiveDatabaseSelectionProvider
例如,该 Bean 可以使用 Spring 的安全上下文来检索租户。 下面是使用 Spring 安全性保护的命令式应用程序的工作示例:
清单 48.Neo4jConfig.java
@Configuration
public class Neo4jConfig {
@Bean
DatabaseSelectionProvider databaseSelectionProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast)
.map(User::getUsername).map(DatabaseSelection::byName).orElseGet(DatabaseSelection::undecided);
}
}
请注意不要将从一个数据库检索到的实体与另一个数据库混淆。 每个新事务都会请求数据库名称,因此在两次调用之间更改数据库名称时,最终可能会得到比预期更少或更多的实体。 或者更糟糕的是,您可能不可避免地将错误的实体存储在错误的数据库中。 |
Spring Boot Neo4j 健康指示器针对默认数据库,如何更改它?
Spring Boot 带有命令式和反应式 Neo4j健康指示器。这两种变体都能够检测应用程序上下文中的多个 bean,并提供 对每个实例的整体运行状况的贡献。 但是,Neo4j驱动程序确实连接到服务器,而不是该服务器中的特定数据库。 Spring Boot能够在没有Spring Data Neo4j的情况下配置驱动程序,并作为要使用的数据库的信息。 绑定到Spring Data Neo4j,此信息不适用于内置健康指示器。org.neo4j.driver.Driver
在许多部署方案中,这很可能不是问题。 但是,如果配置的数据库用户至少没有对默认数据库的访问权限,则运行状况检查将失败。
这可以通过知道数据库选择的自定义 Neo4j 健康贡献者来缓解。
命令式变体
public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {
private final Driver driver;
private final DatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try {
SessionConfig sessionConfig = Optional
.ofNullable(databaseSelectionProvider.getDatabaseSelection())
.filter(databaseSelection -> databaseSelection != DatabaseSelection.undecided())
.map(DatabaseSelection::getValue)
.map(v -> SessionConfig.builder().withDatabase(v).build())
.orElseGet(SessionConfig::defaultConfig);
class Tuple {
String edition;
ResultSummary resultSummary;
Tuple(String edition, ResultSummary resultSummary) {
this.edition = edition;
this.resultSummary = resultSummary;
}
}
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
Tuple health = driver.session(sessionConfig)
.writeTransaction(tx -> {
Result result = tx.run(query);
String edition = result.single().get("edition").asString();
return new Tuple(edition, result.consume());
});
addHealthDetails(builder, health.edition, health.resultSummary);
} catch (Exception ex) {
builder.down().withException(ex);
}
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
这将使用可用的数据库选择来运行与 Boot 运行的相同查询,以检查连接是否正常。 使用以下配置来应用它:
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean
DatabaseSelectionAwareNeo4jHealthIndicator databaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, databaseSelectionProvider);
}
@Bean
HealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators) {
return CompositeHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean
InitializingBean healthContributorRegistryCleaner(
HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
|
如果您有多个驱动程序和数据库选择提供程序,则需要为每个组合创建一个指示器
|
|
这可确保所有这些指标都分组在 Neo4j 下,取代默认的 Neo4j 健康指标
|
|
这可以防止单个参与者直接显示在运行状况终结点中
|
反应性变体
反应式变体基本相同,使用反应式类型和相应的反应式基础设施类:
public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
extends AbstractReactiveHealthIndicator {
private final Driver driver;
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jReactiveHealthIndicator(
Driver driver,
ReactiveDatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
return databaseSelectionProvider.getDatabaseSelection()
.map(databaseSelection -> databaseSelection == DatabaseSelection.undecided() ?
SessionConfig.defaultConfig() :
SessionConfig.builder().withDatabase(databaseSelection.getValue()).build()
)
.flatMap(sessionConfig ->
Mono.usingWhen(
Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
s -> {
Publisher<Tuple2<String, ResultSummary>> f = s.readTransaction(tx -> {
RxResult result = tx.run(query);
return Mono.from(result.records())
.map((record) -> record.get("edition").asString())
.zipWhen((edition) -> Mono.from(result.consume()));
});
return Mono.fromDirect(f);
},
RxSession::close
)
).map((result) -> {
addHealthDetails(builder, result.getT1(), result.getT2());
return builder.build();
});
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
当然,还有配置的反应式变体。它需要两个不同的注册表清理器,因为Spring Boot将 包装现有的反应指标,以便与非反应式执行器端点一起使用。
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean
ReactiveHealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return CompositeReactiveHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean
InitializingBean healthContributorRegistryCleaner(HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
@Bean
InitializingBean reactiveHealthContributorRegistryCleaner(
ReactiveHealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
Neo4j 4.4 支持模拟不同的用户 - 如何使用它们?
用户模拟在大型多租户设置中特别有趣,其中物理连接(或技术) 用户可以模拟多个租户。根据您的设置,这将大大减少所需的物理驱动程序实例数。
该功能需要服务器端的 Neo4j 企业版 4.4+,客户端(或更高版本)需要 4.4+ 驱动程序。org.neo4j.driver:neo4j-java-driver:4.4.0
对于命令式和反应式版本,您需要分别提供 a。 相同的实例需要传递给它们的反应变体。UserSelectionProvider
ReactiveUserSelectionProvider
Neo4Client
Neo4jTransactionManager
在无引导命令式和反应式配置中,您只需要提供 有问题的类型:
清单 49.用户选择提供程序 Bean
public class CustomConfig {
@Bean
public UserSelectionProvider getUserSelectionProvider() {
return () -> UserSelection.impersonate("someUser");
}
}
在典型的 Spring 引导场景中,此功能需要更多的工作,因为引导还支持没有该功能的 SDN 版本。 因此,给定清单 49 中的 bean,您需要完全自定义客户端和事务管理器:
清单 50.春季启动的必要定制
public class CustomConfig {
@Bean
public Neo4jClient neo4jClient(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
@Bean
public PlatformTransactionManager transactionManager(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jTransactionManager
.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
}
使用来自Spring Data Neo4j的Neo4j集群实例
以下问题适用于 Neo4j AuraDB 以及 Neo4j 本地集群实例。
我是否需要特定的配置才能使事务与 Neo4j 因果集群无缝工作?
不,你没有。 SDN在内部使用Neo4j因果集群书签,无需您进行任何配置。 同一线程或同一反应式流中的事务将能够像您预期的那样读取其先前更改的值。
对 Neo4j 集群使用只读事务重要吗?
是的,它是。 Neo4j集群架构是一种因果集群架构,它区分了主服务器和辅助服务器。 主服务器可以是单个实例,也可以是核心实例。它们都可以回答读取和写入操作。 写入操作从核心实例传播到集群内的只读副本或更一般的追随者。 这些关注者是辅助服务器。 辅助服务器不应答写入操作。
在标准部署方案中,群集中将有一些核心实例和许多只读副本。 因此,请务必将操作或查询标记为只读,以便以领导者 永远不会不堪重负,查询尽可能多地传播到只读副本。
Spring Data Neo4j和底层Java驱动程序都不做Cypher解析,两个构建块都假设 默认情况下的写入操作。做出此决定是为了支持所有开箱即用的操作。如果某些内容在 默认情况下,堆栈将假定为只读,堆栈最终可能会向只读副本发送写入查询并失败 在执行它们时。
默认情况下,所有、 和预定义的存在方法都标记为只读。findById findAllById findAll |
下面介绍了一些选项:
清单 51.将整个存储库设为只读
@Transactional(readOnly = true)
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
清单 52.将选定的存储库方法设为只读
interface PersonRepository extends Neo4jRepository<Person, Long> {
@Transactional(readOnly = true)
Person findOneByName(String name);
@Transactional(readOnly = true)
@Query("""
CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her')
YIELD node AS n RETURN n""")
Person findByCustomQuery();
}
为什么此只读不是默认的?虽然它适用于上面的派生查找器(我们实际上知道它是只读的), 我们经常看到用户添加自定义并通过aconstruct实现它的情况, 这当然是写入操作。@Query MERGE |
自定义过程可以做各种各样的事情,目前没有办法为我们检查只读与写在这里。 |
清单 53.编排从服务到存储库的调用
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
interface MovieRepository extends Neo4jRepository<Movie, Long> {
List<Movie> findByLikedByPersonName(String name);
}
public class PersonService {
private final PersonRepository personRepository;
private final MovieRepository movieRepository;
public PersonService(PersonRepository personRepository,
MovieRepository movieRepository) {
this.personRepository = personRepository;
this.movieRepository = movieRepository;
}
@Transactional(readOnly = true)
public Optional<PersonDetails> getPerson(Long id) {
return this.repository.findById(id)
.map(person -> {
var movies = this.movieRepository
.findByLikedByPersonName(person.getName());
return new PersonDetails(person, movies);
});
}
}
在这里,对多个存储库的多个调用包装在一个只读事务中。 |
清单 54.使用Springsinside私有服务方法和/或使用Neo4j客户端TransactionTemplate
public class PersonService {
private final TransactionTemplate readOnlyTx;
private final Neo4jClient neo4jClient;
public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {
this.readOnlyTx = new TransactionTemplate(transactionManager,
new TransactionDefinition() {
@Override public boolean isReadOnly() {
return true;
}
}
);
this.neo4jClient = neo4jClient;
}
void internalOperation() {
Collection<Node> nodes = this.readOnlyTx.execute(state -> {
return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class)
.mappedBy((types, record) -> record.get(0).asNode())
.all();
});
}
}
创建一个具有您需要的特征的实例。 当然,这也可以是全球豆。TransactionTemplate |
使用事务模板的第一个原因:声明性事务不起作用 在包私有或私有方法中,也不在内部方法调用中(想象一下另一种方法 在此服务调用中),因为它们的性质是通过方面实现的 和代理。internalOperation |
这是SDN提供的固定实用程序。它不能被注释,但它与Spring集成在一起。 因此,它为您提供了使用纯驱动程序所做的一切,并且没有自动映射和 交易。它还服从声明性事务。Neo4jClient |
我可以检索最新的书签或为事务管理器设定种子吗?
正如书签管理中简要提到的,无需配置有关书签的任何内容。 但是,检索从数据库接收的 SDN 事务系统的最新书签可能很有用。 您可以添加 alike:@Bean
BookmarkCapture
清单 55.书签捕获.java
public final class BookmarkCapture
implements ApplicationListener<Neo4jBookmarksUpdatedEvent> {
@Override
public void onApplicationEvent(Neo4jBookmarksUpdatedEvent event) {
// We make sure that this event is called only once,
// the thread safe application of those bookmarks is up to your system.
Set<Bookmark> latestBookmarks = event.getBookmarks();
}
}
要为事务系统设定种子,需要如下所示的自定义事务管理器:
清单 56.书签种子配置.java
@Configuration
public class BookmarkSeedingConfig {
@Bean
public PlatformTransactionManager transactionManager(
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
Supplier<Set<Bookmark>> bookmarkSupplier = () -> {
Bookmark a = null;
Bookmark b = null;
return Set.of(a, b);
};
Neo4jBookmarkManager bookmarkManager =
Neo4jBookmarkManager.create(bookmarkSupplier);
return new Neo4jTransactionManager(
driver, databaseNameProvider, bookmarkManager);
}
}
让春天注入那些 |
该供应商可以是任何包含您要带入系统的最新书签的人 |
使用它创建书签管理器 |
将其传递给自定义事务管理器 |
无需执行上述任何操作,除非您的应用程序需要访问或提供 此数据。如果有疑问,也不要这样做。 |
我可以禁用书签管理吗?
我们提供了一个Noop书签管理器,可以有效地禁用书签管理。
使用此书签管理器的风险自负,它将通过删除所有书签来有效地禁用任何书签管理 书签,从不提供任何书签。在集群中,您将面临过时读取的高风险。在单个 实例 它很可能不会有任何区别。 在集群中,这仅是一种明智的方法,并且只要您可以容忍过时的读取并且没有危险 覆盖旧数据。 |
您需要在系统中提供以下配置,并确保 SDN 使用事务管理器:
清单 57.书签已禁用配置.java
@Configuration
public class BookmarksDisabledConfig {
@Bean
public PlatformTransactionManager transactionManager(
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
Neo4jBookmarkManager bookmarkManager = Neo4jBookmarkManager.noop();
return new Neo4jTransactionManager(
driver, databaseNameProvider, bookmarkManager);
}
}
我需要使用 Neo4j 特定的注释吗?
不。 您可以自由使用以下等效的 Spring 数据注释:
特定于 SDN 的注释 |
弹簧数据通用注释 |
目的 |
差异 |
org.springframework.data.neo4j.core.schema.Id |
org.springframework.data.annotation.Id |
将带的属性标记为唯一 ID。 |
特定没有附加功能。 |
org.springframework.data.neo4j.core.schema.Node |
org.springframework.data.annotation.Persistent |
将类标记为持久实体。 |
@Node 允许自定义标签 |
如何使用分配的 ID?
只需使用without并通过构造函数参数或setter或wither填充您的id属性。 有关查找良好 ID 的一些一般评论,请参阅此博客文章。@Id
@GeneratedValue
如何使用外部生成的 ID?
我们提供接口。 以您想要的任何方式实现它,并像这样配置您的实现:org.springframework.data.neo4j.core.schema.IdGenerator
清单 58.ThingWithGenerated Id.java
@Node
public class ThingWithGeneratedId {
@Id @GeneratedValue(TestSequenceGenerator.class)
private String theId;
}
如果将类的名称传递给,则此类必须具有 no-args 默认构造函数。 但是,您也可以使用字符串:@GeneratedValue
清单 59.ThingWithIdGenerated ByBean.java
@Node
public class ThingWithIdGeneratedByBean {
@Id @GeneratedValue(generatorRef = "idGeneratingBean")
private String theId;
}
用这个,指春天语境中的豆子。 这对于序列生成可能很有用。idGeneratingBean
我是否必须为每个域类创建存储库?
不。 看看SDN构建块并找到理论。Neo4jTemplate
ReactiveNeo4jTemplate
这些模板了解您的域,并提供用于检索、写入和计数实体的所有必要的基本 CRUD 方法。
这是我们使用命令式模板的规范电影示例:
清单 60.模板示例测试.java
@DataNeo4jTest
public class TemplateExampleTest {
@Test
void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, "
+ "Jim Douglas (Dean Jones), and Jim's love interest, " + "Carole Bennett (Michele Lee)");
Roles roles1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles roles2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
neo4jTemplate.save(movie);
Optional<PersonEntity> person = neo4jTemplate.findById("Dean Jones", PersonEntity.class);
assertThat(person).map(PersonEntity::getBorn).hasValue(1931);
assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L);
}
}
这是反应式版本,为简洁起见省略了设置:
清单 61.反应式模板示例测试.java
@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {
@Container private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.4");
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("org.neo4j.driver.uri", neo4jContainer::getBoltUrl);
registry.add("org.neo4j.driver.authentication.username", () -> "neo4j");
registry.add("org.neo4j.driver.authentication.password", neo4jContainer::getAdminPassword);
}
@Test
void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, Jim Douglas (Dean Jones), and Jim's love interest, Carole Bennett (Michele Lee)");
Roles role1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles role2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(role1);
movie.getActorsAndRoles().add(role2);
StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete();
StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn))
.expectNext(1931).verifyComplete();
StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete();
}
}
请注意,这两个示例都使用Spring Boot。@DataNeo4jTest
如何将自定义查询与存储库方法返回器一起使用?Page<T>
Slice<T>
虽然您不必在派生查找器方法上提供 aas 参数之外的任何其他内容 返回 aor a,您必须准备自定义查询来处理可分页。清单 62概述了所需的内容。Pageable
Page<T>
Slice<T>
清单 62.页面和切片
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
Page<Person> findByName(String name, Pageable pageable);
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit"
)
Slice<Person> findSliceByName(String name, Pageable pageable);
@Query(
value = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
countQuery = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
)
Page<Person> findPageByName(String name, Pageable pageable);
}
为您创建查询的派生查找器方法。 它为您处理。 应使用排序的可分页对象。Pageable |
此方法用于定义自定义查询。它返回 a。 切片不知道总页数,因此自定义查询 不需要专用的计数查询。SDN 将通知您它估计下一个切片。 密码模板必须同时发现和密码参数。 如果省略它们,SDN 将发出警告。可能不会符合您的期望。 此外,应该未排序,您应该提供稳定的订单。 我们不会使用可分页项中的排序信息。@Query Slice<Person> $skip $limit Pageable |
此方法返回一个页面。页面知道总页数的确切数量。 因此,必须指定其他计数查询。 第二种方法的所有其他限制均适用。 |
我可以映射命名路径吗?
一系列连接的节点和关系在Neo4j中称为“路径”。 Cypher 允许使用标识符命名路径,例如:
或者像臭名昭著的电影图表一样,其中包括以下路径(在这种情况下,两个演员之间的最短路径之一):
清单 63.“培根”距离
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p
看起来像这样:
![Spring Data(数据)Neo4j(二)_xml_03](//dev-img.mos.moduyun.com/20231025/cab528d3-b553-44f7-bd88-fb08cf675b44.png)
我们发现 3 个节点被标记,2 个节点被标记。两者都可以使用自定义查询进行映射。 假设有一个节点实体用于两者以及处理关系:Vertex
Movie
Vertex
Movie
Actor
清单 64.“标准”电影图域模型
@Node
public final class Person {
@Id @GeneratedValue
private final Long id;
private final String name;
private Integer born;
@Relationship("REVIEWED")
private List<Movie> reviewed = new ArrayList<>();
}
@RelationshipProperties
public final class Actor {
@RelationshipId
private final Long id;
@TargetNode
private final Person person;
private final List<String> roles;
}
@Node
public final class Movie {
@Id
private final String title;
@Property("tagline")
private final String description;
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
private final List<Actor> actors;
}
当对类型如下的域类使用清单63中所示的查询时Vertex
interface PeopleRepository extends Neo4jRepository<Person, Long> {
@Query(""
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "RETURN p"
)
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}
它将从路径中检索所有人并映射他们。 如果路径上存在关系类型,例如域中也存在这些关系类型,则这些 将从路径中相应地填充。REVIEWED
使用从基于路径的查询冻结的节点来保存数据时要特别小心。 如果并非所有关系都处于水合状态,则数据将丢失。 |
反之亦然。可以对实体使用相同的查询。 然后,它将仅填充电影。 下面的清单显示了如何执行此操作以及如何使用其他数据丰富查询 在路径上找不到。该数据用于正确填充缺失的关系(在这种情况下,所有参与者)Movie
interface MovieRepository extends Neo4jRepository<Movie, String> {
@Query(""
+ "MATCH p=shortestPath(\n"
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
+ "UNWIND x AS m\n"
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
+ "RETURN p, collect(r), collect(d)"
)
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}
该查询返回路径以及收集的所有关系和相关节点,以便电影实体完全冻结。
路径映射也适用于单个路径以及多个路径记录(由函数返回)。allShortestPath
可以有效地使用命名路径来填充和返回不仅仅是根节点的多个节点,请参阅使用路径填充和返回实体列表。 |
是使用自定义查询的唯一方法吗?@Query
否,不是运行自定义查询的唯一方法。 在自定义查询完全填充域的情况下,注释很舒适。 请记住,SDN 6 假定映射的域模型是真实的。 这意味着,如果您使用仅部分填充模型的自定义查询,则有可能使用相同的查询 对象写回数据,最终将擦除或覆盖您在查询中未考虑的数据。@Query
@Query
因此,请在结果形状类似于您的域的所有情况下使用存储库和声明性方法 模型,或者您确定不使用部分映射的模型来编写命令。@Query
有哪些替代方案?
- 投影可能已经足以塑造您在图形上的视图:它们可用于定义 以显式方式获取属性和相关实体的深度:通过对它们进行建模。
- 如果您的目标是仅使查询的条件动态化,那么请查看QuerydslPredicateExecutor,尤其是我们自己的变体。两种混合都允许添加条件 我们为您创建的完整查询。因此,您将使用自定义条件完全填充域。 当然,您的条件必须与我们生成的内容相适应。在此处查找根节点的名称、相关节点等。CypherdslConditionExecutor
- 使用Cypher-DSL通过theor。 Cypher-DSL注定要创建动态查询。最后,无论如何,这就是SDN在引擎盖下使用的东西。相应的 mixins 既适用于存储库本身的域类型,也适用于投影(mixins 用于添加的内容) 条件没有)。
CypherdslStatementExecutor
ReactiveCypherdslStatementExecutor
如果您认为可以通过部分动态查询或完全动态查询以及投影来解决问题, 请跳回有关Spring Data Neo4j Mixins的章节。
否则,请阅读两件事:自定义存储库碎片化我们在SDN 6中提供的抽象级别。
为什么现在谈论自定义存储库片段?
- 您可能遇到更复杂的情况,其中需要多个动态查询,但查询仍属于 从概念上讲,在存储库中而不是在服务层中
- 自定义查询返回的图形形状结果与域模型不太吻合 因此,自定义查询也应附带自定义映射
- 您需要与驱动程序交互,即对于不应通过对象映射的批量加载。
假设以下存储库声明基本上聚合了一个基本存储库加上 3 个片段:
清单 65.由多个片段组成的存储库
public interface MovieRepository extends Neo4jRepository<MovieEntity, String>,
DomainResults,
NonDomainResults,
LowlevelInteractions {
}
存储库包含电影,如入门部分所示。
存储库从中扩展的附加接口(,和) 是解决上述所有问题的片段。DomainResults
NonDomainResults
LowlevelInteractions
使用复杂的动态自定义查询,但仍返回域类型
片段声明了一个额外的方法:DomainResults
findMoviesAlongShortestPath
清单 66.域结果片段
interface DomainResults {
@Transactional(readOnly = true)
List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to);
}
此方法带有注释,以表示读者可以回答。 它不能由 SDN 派生,但需要自定义查询。 此自定义查询由该接口的一个实现提供。 该实现与后缀具有相同的名称:@Transactional(readOnly = true)
Impl
清单 67.使用 Neo4jTemplate 的片段实现
class DomainResultsImpl implements DomainResults {
private final Neo4jTemplate neo4jTemplate;
DomainResultsImpl(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
@Override
public List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) {
Node p1 = node("Person").withProperties("name", parameter("person1"));
Node p2 = node("Person").withProperties("name", parameter("person2"));
NamedPath shortestPath = shortestPath("p").definedBy(
p1.relationshipBetween(p2).unbounded()
);
Expression p = shortestPath.getRequiredSymbolicName();
Statement statement = Cypher.match(shortestPath)
.with(p, listWith(name("n"))
.in(Functions.nodes(shortestPath))
.where(anyNode().named("n").hasLabels("Movie")).returning().as("mn")
)
.unwind(name("mn")).as("m")
.with(p, name("m"))
.match(node("Person").named("d")
.relationshipTo(anyNode("m"), "DIRECTED").named("r")
)
.returning(p, Functions.collect(name("r")), Functions.collect(name("d")))
.build();
Map<String, Object> parameters = new HashMap<>();
parameters.put("person1", from.getName());
parameters.put("person2", to.getName());
return neo4jTemplate.findAll(statement, parameters, MovieEntity.class);
}
}
|
这是由运行时通过构造函数注入的。没必要。Neo4jTemplate DomainResultsImpl @Autowired |
|
Cypher-DSL用于构建一个复杂的语句(与路径映射中所示的几乎相同)。 该语句可以直接传递到模板。 |
该模板还具有基于字符串的查询的重载,因此您也可以将查询写为字符串。 这里的重要要点是:
- 模板“知道”您的域对象并相应地映射它们
-
@Query
不是定义自定义查询的唯一选项
- 它们可以通过各种方式生成
- 注释受到尊重
@Transactional
使用自定义查询和自定义映射
通常,自定义查询指示自定义结果。 所有这些结果都应该映射为吗?当然不是!很多时候,这些对象表示读取命令 并且不应用作写入命令。 SDN 6不能或不想不映射Cypher可能出现的一切,这也并非不可能。 但是,它确实提供了几个钩子来运行您自己的映射:在。 在驱动程序上使用 SDN 6 的好处:@Node
Neo4jClient
Neo4jClient
- Theis与Springs交易管理集成
Neo4jClient
- 它有一个流畅的API来绑定参数
- 它有一个流畅的API,公开记录和Neo4j类型系统,以便您可以访问 结果中的所有内容以执行映射
声明片段与以前完全相同:
清单 68.声明非域类型结果的片段
interface NonDomainResults {
class Result {
public final String name;
public final String typeOfRelation;
Result(String name, String typeOfRelation) {
this.name = name;
this.typeOfRelation = typeOfRelation;
}
}
@Transactional(readOnly = true)
Collection<Result> findRelationsToMovie(MovieEntity movie);
}
这是一个虚构的非域结果。实际查询结果可能看起来更复杂。 |
此片段添加的方法。同样,该方法用 Spring 的@Transactional |
如果没有该片段的实现,启动将失败,因此这里是:
清单 69.使用 Neo4jClient 的片段实现
class NonDomainResultsImpl implements NonDomainResults {
private final Neo4jClient neo4jClient;
NonDomainResultsImpl(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public Collection<Result> findRelationsToMovie(MovieEntity movie) {
return this.neo4jClient
.query(""
+ "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) "
+ "RETURN people.name AS name, "
+ " Type(relatedTo) as typeOfRelation"
)
.bind(movie.getTitle()).to("title")
.fetchAs(Result.class)
.mappedBy((typeSystem, record) -> new Result(record.get("name").asString(),
record.get("typeOfRelation").asString()))
.all();
}
}
在这里,我们使用由基础架构提供的。Neo4jClient |
客户端只接受字符串,但在呈现为字符串时仍然可以使用 Cypher-DSL |
将单个值绑定到命名参数。还有一个重载来绑定整个参数映射 |
这是您想要的结果类型 |
最后,该方法,如果需要,为结果中的每个条目以及驱动程序类型系统公开一个。 这是您在其中挂钩自定义映射的 APImappedBy Record |
整个查询在 Spring 事务的上下文中运行,在本例中为只读事务。
低级交互
有时您可能希望从存储库进行批量加载或删除整个子图或以非常特定的方式进行交互 使用Neo4j Java-Driver。这也是可能的。以下示例演示如何:
清单 70.使用普通驱动程序的片段
interface LowlevelInteractions {
int deleteGraph();
}
class LowlevelInteractionsImpl implements LowlevelInteractions {
private final Driver driver;
LowlevelInteractionsImpl(Driver driver) {
this.driver = driver;
}
@Override
public int deleteGraph() {
try (Session session = driver.session()) {
SummaryCounters counters = session
.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume())
.counters();
return counters.nodesDeleted() + counters.relationshipsDeleted();
}
}
}
|
直接与驱动程序合作。与所有示例一样:没有必要形成。所有片段 实际上是可以自行测试的。@Autowired |
|
用例是虚构的。这里我们使用一个驱动程序管理的事务删除整个图形并返回 已删除的节点和关系
|
这种交互当然不会在 Spring 事务中运行,因为驱动程序不知道 Spring。
综上所述,此测试成功:
清单 71.测试组合存储库
@Test
void customRepositoryFragmentsShouldWork(
@Autowired PersonRepository people,
@Autowired MovieRepository movies
) {
PersonEntity meg = people.findById("Meg Ryan").get();
PersonEntity kevin = people.findById("Kevin Bacon").get();
List<MovieEntity> moviesBetweenMegAndKevin = movies.
findMoviesAlongShortestPath(meg, kevin);
assertThat(moviesBetweenMegAndKevin).isNotEmpty();
Collection<NonDomainResults.Result> relatedPeople = movies
.findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
assertThat(relatedPeople).isNotEmpty();
assertThat(movies.deleteGraph()).isGreaterThan(0);
assertThat(movies.findAll()).isEmpty();
assertThat(people.findAll()).isEmpty();
}
最后一句话:Spring Data Neo4j会自动获取所有三个接口和实现。 无需进一步配置。 此外,可以只用一个额外的片段(定义所有三种方法的接口)创建相同的整体存储库 和一个实现。该实现将注入所有三个抽象(模板、客户端和驱动程序)。
当然,所有这些也适用于反应式存储库。 他们将使用驱动程序提供的响应式会话。ReactiveNeo4jTemplate
ReactiveNeo4jClient
如果所有存储库都有重复的方法,则可以换出默认存储库实现。
如何使用自定义的Spring Data Neo4j基础存储库?
与共享的 Spring 数据共享文档在第9.6.2 节中显示的 Spring 数据 JPA 的方式基本相同。 只有在我们的情况下,您才会从
清单 72.自定义基础存储库
public class MyRepositoryImpl<T, ID> extends SimpleNeo4jRepository<T, ID> {
MyRepositoryImpl(
Neo4jOperations neo4jOperations,
Neo4jEntityInformation<T, ID> entityInformation
) {
super(neo4jOperations, entityInformation);
}
@Override
public List<T> findAll() {
throw new UnsupportedOperationException("This implementation does not support `findAll`");
}
}
基类需要此签名。取(实际规格的) 和实体信息,并在需要时将它们存储在属性上。Neo4jOperations Neo4jTemplate |
在这个例子中,我们禁止使用该方法。 您可以添加获取深度的方法,并根据该深度运行自定义查询。 一种方法如清单 66 所示。findAll
要为所有声明的存储库启用此基本存储库,请使用以下内容启用 Neo4j 存储库:@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class)
如何审核实体?
支持所有 Spring 数据注释。 那些是
-
org.springframework.data.annotation.CreatedBy
-
org.springframework.data.annotation.CreatedDate
-
org.springframework.data.annotation.LastModifiedBy
-
org.springframework.data.annotation.LastModifiedDate
第12章为您提供了如何在Spring Data Commons的更大上下文中使用审计的一般视图。 下面的清单列出了Spring Data Neo4j提供的所有配置选项:
清单 73.启用和配置 Neo4j 审计
@Configuration
@EnableNeo4jAuditing(
modifyOnCreate = false,
auditorAwareRef = "auditorProvider",
dateTimeProviderRef = "fixedDateTimeProvider"
)
class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of("A user");
}
@Bean
public DateTimeProvider fixedDateTimeProvider() {
return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
}
}
如果还希望在创建期间写入修改数据,请设置为 true |
使用此属性指定提供审计员的 Bean 的名称(即用户名)
|
使用此属性可以指定提供当前日期的 Bean 的名称。在这种情况下 使用固定日期,因为上述配置是我们测试的一部分
|
反应式版本基本相同,除了审计员感知 bean 的类型, 因此,审计员的检索是反应流的一部分。ReactiveAuditorAware
除了这些审计机制之外,您还可以添加任意数量的 bean 实现或上下文。这些 bean 将被 Spring Data Neo4j 拾取并按顺序调用(以防它们实现 在实体持久化之前进行批注)。BeforeBindCallback<T>
ReactiveBeforeBindCallback<T>
Ordered
@Order
他们可以修改实体或返回一个全新的实体。 下面的示例向上下文添加一个回调,该回调在保留实体之前更改一个属性:
清单 74.保存前修改实体
@Configuration
class CallbacksConfig {
@Bean
BeforeBindCallback<ThingWithAssignedId> nameChanger() {
return entity -> {
ThingWithAssignedId updatedThing = new ThingWithAssignedId(
entity.getTheId(), entity.getName() + " (Edited)");
return updatedThing;
};
}
@Bean
AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
return (entity, definition, source) -> {
entity.setRandomValue(UUID.randomUUID().toString());
return entity;
};
}
}
无需其他配置。
如何使用“按示例查找”?
“按示例查找”是SDN中的一项新功能。 实例化实体或使用现有实体。 使用此实例,您可以创建一个. 如果您的存储库扩展了,您可以立即使用可用的方法,例如清单 75 所示org.springframework.data.domain.Example
org.springframework.data.neo4j.repository.Neo4jRepository
org.springframework.data.neo4j.repository.ReactiveNeo4jRepository
findBy
清单 75.查找示例在操作
Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", null));
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);
movieExample = Example.of(
new MovieEntity("Matrix", null),
ExampleMatcher
.matchingAny()
.withMatcher(
"title",
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
)
);
movies = this.movieRepository.findAll(movieExample);
我需要 Spring Boot 才能使用 Spring Data Neo4j 吗?
不,你没有。 虽然通过 Spring Boot 自动配置许多 Spring 方面可以消除大量手动工作,并且是设置新 Spring 项目的推荐方法,但您不必使用它。
上述解决方案需要以下依赖项:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>6.2.1</version>
</dependency>
Gradle 设置的坐标是相同的。
要选择不同的数据库 - 静态或动态 - 您可以添加Neo4j 4.0 中解释的类型 Bean 支持多个数据库 - 如何使用它们? 对于反应性方案,我们提供。DatabaseSelectionProvider
ReactiveDatabaseSelectionProvider
在没有 Spring 引导的情况下在 Spring 上下文中使用 Spring Data Neo4j
我们提供了两个抽象配置类来支持您引入必要的 bean:用于命令式数据库访问和用于反应式版本。 它们旨在分别使用。 有关示例用法,请参见清单76 和清单 77。 这两个类都要求您重写应该创建驱动程序的内容。org.springframework.data.neo4j.config.AbstractNeo4jConfig
org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig
@EnableNeo4jRepositories
@EnableReactiveNeo4jRepositories
driver()
要获取Neo4j 客户端的命令式版本、模板和对命令式存储库的支持,请使用如下所示的内容:
清单 76.启用 Spring Data Neo4j 基础架构以进行命令式数据库访问
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {
@Override @Bean
public Driver driver() {
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.singletonList(Person.class.getPackage().getName());
}
@Override @Bean
protected DatabaseSelectionProvider databaseSelectionProvider() {
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
驱动程序 Bean 是必需的。 |
这将静态选择名为 and 的数据库是可选的。yourDatabase |
以下清单提供了反应式 Neo4j 客户端和模板,启用反应式事务管理并发现与 Neo4j 相关的存储库:
清单 77.启用 Spring Data Neo4j 基础结构以进行反应式数据库访问
@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {
@Bean
@Override
public Driver driver() {
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.singletonList(Person.class.getPackage().getName());
}
}
在CDI 4.2环境中使用Spring Data Neo0j
为了您的方便,我们提供了一个CDI扩展。 当在兼容的CDI 2.0容器中运行时,它将自动注册并通过Java的服务加载器SPI加载。Neo4jCdiExtension
您唯一需要引入应用程序的是生成 Neo4j Java 驱动程序的注释类型:
清单 78.Neo4j Java驱动程序的CDI制作人
public class Neo4jConfig {
@Produces @ApplicationScoped
public Driver driver() {
return GraphDatabase
.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
public void close(@Disposes Driver driver) {
driver.close();
}
@Produces @Singleton
public DatabaseSelectionProvider getDatabaseSelectionProvider() {
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
与清单 76 中的 plain Spring 相同,但使用相应的 CDI 基础结构进行了注释。 |
这是可选的。但是,如果运行自定义数据库选择提供程序,则不得限定此 Bean。 |
如果您在 SE 容器中运行 - 例如Weld提供的容器,您可以像这样启用扩展:
清单 79.在 SE 容器中启用 Neo4j CDI 扩展
public class SomeClass {
void someMethod() {
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addExtensions(Neo4jCdiExtension.class)
.addBeanClasses(YourDriverFactory.class)
.addPackages(Package.getPackage("your.domain.package"))
.initialize()
) {
SomeRepository someRepository = container.select(SomeRepository.class).get();
}
}
}