領先一步
VMware 提供培訓和認證,以加速您的進度。
了解更多這是一篇由來自 Neo4j 的 Gerrit Meier 所撰寫的客座部落格文章,他維護 Spring Data Neo4j 模組。
幾個星期前,Spring (for) GraphQL 的 1.2.0 版本發布,其中包含許多新功能。這也包括與 Spring Data 模組更完善的整合。在這些變更的推動下,Spring Data Neo4j 中新增了更多支援,以便在使用 Spring GraphQL 時提供最佳體驗。本文將指導您建立一個 Spring 應用程式,該應用程式的資料儲存在 Neo4j 中,並支援 GraphQL。如果您只對該領域的部分感興趣,您可以跳過下一節 ;)
對於這個範例,我選擇深入研究 Fediverse。更具體地說,將一些伺服器和使用者放在焦點上。為什麼選擇這個領域,現在由讀者在接下來的段落中發現。
資料本身與可以從 Mastodon API 取得的屬性對齊。為了保持資料集簡單,資料是手動建立的,而不是獲取所有內容。這會產生一個更容易檢查的資料集。Cypher 匯入語句如下
Cypher 匯入
CREATE (s1:Server {
uri:'mastodon.social', title:'Mastodon', registrations:true,
short_description:'The original server operated by the Mastodon gGmbH non-profit'})
CREATE (meistermeier:Account {id:'106403780371229004', username:'meistermeier', display_name:'Gerrit Meier'})
CREATE (rotnroll666:Account {id:'109258442039743198', username:'rotnroll666', display_name:'Michael Simons'})
CREATE
(meistermeier)-[:REGISTERED_ON]->(s1),
(rotnroll666)-[:REGISTERED_ON]->(s1)
CREATE (s2:Server {
uri:'chaos.social', title:'chaos.social', registrations:false,
short_description:'chaos.social – a Fediverse instance for & by the Chaos community'})
CREATE (odrotbohm:Account {id:'108194553063501090', username:'odrotbohm', display_name:'Oliver Drotbohm'})
CREATE
(odrotbohm)-[:REGISTERED_ON]->(s2)
CREATE
(odrotbohm)-[:FOLLOWS]->(rotnroll666),
(odrotbohm)-[:FOLLOWS]->(meistermeier),
(meistermeier)-[:FOLLOWS]->(rotnroll666),
(meistermeier)-[:FOLLOWS]->(odrotbohm),
(rotnroll666)-[:FOLLOWS]->(meistermeier),
(rotnroll666)-[:FOLLOWS]->(odrotbohm)
CREATE
(s1)-[:CONNECTED_TO]->(s2)
執行語句後,圖形會形成這個形狀。
資料集的圖形檢視
值得注意的資訊是,即使所有使用者都互相追蹤,Mastodon 伺服器也僅以一個方向連接。chaos.social 伺服器上的使用者無法搜尋或瀏覽 mastodon.social 上的時間軸。
免責聲明: 在此範例中,伺服器的聯合使用非雙向關係建立。
若要按照所示範例進行操作,您應使用以下最低版本
最好前往 https://start.spring.io 並建立一個新的專案,其中包含 Spring Data Neo4j 和 Spring GraphQL 相依性。如果您有點懶,也可以從 此連結 下載空專案。
若要 100% 按照範例進行操作,您需要在系統上安裝 Docker。如果您沒有此選項或不想使用 Docker,您可以使用 Neo4j Desktop 或普通的 Neo4j Server 成品進行本機部署,或作為託管選項 Neo4j Aura 或 空的 Neo4j Sandbox。稍後會有一則注意事項說明如何連線到手動啟動的執行個體。不需要使用企業版,所有功能都適用於社群版。
在本範例中,繁重的設定工作將由 Spring Boot 自動設定完成。無需手動設定 Bean。若要了解幕後發生的更多資訊,請參閱 Spring for GraphQL 文件。稍後,將參考文件的特定章節。
首先要做的是對領域類別進行建模。正如匯入中所見,只有 Servers
和 Accounts
。
Account 領域類別
@Node
public class Account {
@Id String id;
String username;
@Property("display_name") String displayName;
@Relationship("REGISTERED_ON") Server server;
@Relationship("FOLLOWS") List<Account> following;
// constructor, etc.
}
可以合理地假設 ID 是(伺服器)唯一的。
Server
中,使用 @Property
將資料庫欄位display_name 對應到 Java 實體中的 camel-case displayName。Server 領域類別
@Node
public class Server {
@Id String uri;
String title;
@Property("registrations") Boolean registrationsAllowed;
@Property("short_description") String shortDescription;
@Relationship("CONNECTED_TO") List<Server> connectedServers;
// constructor, etc.
}
使用這些實體類別,可以建立 AccountRepository
。
Account 儲存庫
@GraphQlRepository
public interface AccountRepository extends Neo4jRepository<Account, String> { }
稍後將說明為什麼使用此註解。此處用於介面的完整性。
若要連線到 Neo4j 執行個體,需要將連線參數新增至application.properties 檔案。
spring.neo4j.uri=neo4j://127.0.0.1:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=verysecret
如果尚未發生,則可以啟動資料庫並執行上面的 Cypher 語句以設定資料。在本文的後面部分,將使用 Neo4j-Migrations 以確保資料庫始終處於所需的狀態。
在研究 Spring Data 和 Spring for GraphQL 的整合功能之前,將使用 @Controller
原型註解類別來設定應用程式。控制器將由 Spring for GraphQL 註冊為查詢 accounts 的 DataFetcher
。
@Controller
class AccountController {
private final AccountRepository repository;
AccountController(AccountRepository repository) {
this.repository = repository;
}
@QueryMapping
List<Account> accounts() {
return repository.findAll();
}
}
定義 GraphQL 結構描述,該結構描述不僅定義我們的實體,還定義與控制器中的方法(accounts)同名的查詢。
type Query {
accounts: [Account]!
}
type Account {
id: ID!
username: String!
displayName: String!
server: Server!
following: [Account]
lastMessage: String!
}
type Server {
uri: ID!
title: String!
shortDescription: String!
connectedServers: [Server]
}
此外,為了以簡單的方式瀏覽 GraphQL 資料,應在application.properties 中啟用 GraphiQL。這是在開發期間很有用的工具。通常應該為生產部署停用此功能。
spring.graphql.graphiql.enabled=true
如果所有設定都如上所述,可以使用 ./mvnw spring-boot:run
啟動應用程式。瀏覽至 https://127.0.0.1:8080/graphiql?path=/graphql 將顯示 GraphiQL 瀏覽器。
在 GraphiQL 中查詢
若要驗證 accounts
方法是否有效,GraphQL 請求會傳送至應用程式。
第一個 GraphQL 請求
{
accounts {
username
}
}
並且預期的答案會從伺服器傳回。
GraphQL 回應
{
"data": {
"accounts": [
{
"username": "meistermeier"
},
{
"username": "rotnroll666"
},
{
"username": "odrotbohm"
}
]
}
}
當然,可以透過新增參數以使用 @Argument
尊重引數,或取得請求的欄位(此處為 accounts.username),以壓縮透過網路傳輸的資料量,來調整控制器中的方法。在先前的範例中,儲存庫將會擷取給定領域實體的所有屬性,包括所有關聯性。大部分資料將被丟棄,僅將username 傳回給使用者。
這個範例應該能讓人了解使用 Annotated Controllers 可以完成的事情。透過 Spring Data Neo4j 的查詢產生和映射功能,建立了一個(簡單的)GraphQL 應用程式。
但在這個階段,這兩個函式庫似乎在這個應用程式中並行運作,還不像是一個整合。如何才能讓 SDN 和 Spring for GraphQL 真正地結合在一起呢?
第一步,可以刪除 AccountController
中的 accounts
方法。重新啟動應用程式,並使用上面的請求再次查詢,仍然會得到相同的結果。
之所以能運作,是因為 Spring for GraphQL 識別了 GraphQL schema 中的結果類型(Account
的陣列)。它會掃描符合該類型的合格 Spring Data 儲存庫。這些儲存庫必須擴展 QueryByExampleExecutor
或 QuerydslPredicateExecutor
(不包含在本篇部落格文章中) 以處理給定的類型。在本範例中,AccountRepository
已經隱式地標記為 QueryByExampleExecutor
,因為它擴展了 Neo4jRespository
,而後者已經定義了執行器。 @GraphQlRepository
註解使 Spring for GraphQL 知道這個儲存庫可以並且應該用於查詢(如果可能)。
在沒有對實際程式碼進行任何變更的情況下,可以在 schema 中定義第二個 *Query 欄位*。這次應該根據使用者名稱過濾結果。乍看之下,使用者名稱看起來是唯一的,但在 Fediverse 中,這僅適用於給定的實例。多個實例可能具有完全相同的使用者名稱。為了尊重這種行為,查詢應該能夠返回一個 Accounts
陣列。
有關 query by example (Spring Data commons) 的文件提供了有關此機制的內部運作的更多詳細資訊。
更新後的查詢類型
type Query {
account(username: String!): [Account]!
重新啟動應用程式現在會顯示一個選項,可以互動式地將使用者名稱作為參數添加到查詢中。
查詢相同使用者名稱的陣列
{
account(username: "meistermeier") {
username
following {
username
server {
uri
}
}
}
}
顯然,只有一個具有此使用者名稱的 Account
。
依使用者名稱查詢的回應
{
"data": {
"account": [
{
"username": "meistermeier",
"following": [
{
"username": "rotnroll666",
"server": {
"uri": "mastodon.social"
}
},
{
"username": "odrotbohm",
"server": {
"uri": "chaos.social"
}
}
]
}
]
}
}
在幕後,Spring for GraphQL 會將該欄位作為參數添加到傳遞到儲存庫作為範例的物件中。然後,Spring Data Neo4j 檢查該範例並為 Cypher 查詢建立匹配條件,執行該查詢並將結果發送回 Spring GraphQL 以進行進一步處理,將結果塑造成正確的回應格式。
(示意) API 呼叫流程
雖然範例資料集不是很大,但擁有一個適當的功能來允許以區塊形式請求結果資料通常很有用。 Spring for GraphQL 使用 Cursor Connections 規範。
包含所有類型的完整 schema 規範如下。
具有游標連線的 Schema
type Query {
accountScroll(username:String, first: Int, after: String, last: Int, before:String): AccountConnection
}
type AccountConnection {
edges: [AccountEdge]!
pageInfo: PageInfo!
}
type AccountEdge {
node: Account!
cursor: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
type Account {
id: ID!
username: String!
displayName: String!
server: Server!
following: [Account]
lastMessage: String!
}
type Server {
uri: ID!
title: String!
shortDescription: String!
connectedServers: [Server]
}
即使我個人喜歡擁有一個完整的有效 schema,也可以跳過定義中的所有 *Cursor Connections* 特定部分。只需具有 AccountConnection
定義的查詢就足以讓 Spring for GraphQL 推導和填寫遺失的部分。參數的解讀如下
first
:如果沒有預設值,則要提取的資料量after
:應提取資料的捲動位置last
:在 before
位置之前要提取的資料量before
:應提取資料直到 (不包含) 的捲動位置還有一個問題:結果集以什麼順序返回?在這種情況下,穩定的排序順序是*必須*的,否則無法保證資料庫以可預測的順序返回資料。儲存庫還需要擴展 QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer
並實作 customize
方法。在其中,還可以為查詢新增預設限制,在本例中為 *1* 以顯示分頁的運作方式。
已新增排序順序(和限制)
@GraphQlRepository
interface AccountRepository extends Neo4jRepository<Account, String>,
QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer<Account>
{
@Override
default QueryByExampleDataFetcher.Builder<Account, ?> customize(QueryByExampleDataFetcher.Builder<Account, ?> builder) {
return builder.sortBy(Sort.by("username"))
.defaultScrollSubrange(new ScrollSubrange(ScrollPosition.offset(), 1, true));
}
}
應用程式重新啟動後,現在可以呼叫第一個分頁查詢。
第一個元素的分頁
{
accountScroll {
edges {
node {
username
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
為了取得進一步互動的中繼資料,也要求了 pageInfo
的某些部分。
第一個元素的結果
{
"data": {
"accountScroll": {
"edges": [
{
"node": {
"username": "meistermeier"
}
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "T18x"
}
}
}
}
現在 endCursor
可以用於下一個互動。 使用此值作為 *after* 的值並限制為 2 來查詢應用程式...
最後一個元素的分頁
{
accountScroll(after:"T18x", first: 2) {
edges {
node {
username
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
...產生最後一個元素。此外,沒有下一頁 (hasNextPage=false
) 的標記表示分頁已到達資料集的末尾。
最後一個元素的結果
{
"data": {
"accountScroll": {
"edges": [
{
"node": {
"username": "odrotbohm"
}
},
{
"node": {
"username": "rotnroll666"
}
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": "T18z"
}
}
}
}
也可以使用定義的 last
和 before
參數向後捲動資料。此外,將此捲動與已知的 query by example 功能結合使用,並在 GraphQL schema 中定義一個也接受 Account
欄位作為篩選條件的查詢是完全有效的。
具有分頁功能的篩選器
accountScroll(username:String, first: Int, after: String, last: Int, before:String): AccountConnection
使用 GraphQL 的一大優勢是可以引入聯合資料。簡而言之,這意味著儲存在應用程式資料庫中的資料可以透過來自遠端系統/微服務/的資料進行豐富,例如在本例中。
可以透過使用已經定義的控制器來實作此資料聯合。
聯合資料的 SchemaMapping
@Controller
class AccountController {
@SchemaMapping
String lastMessage(Account account) {
var id = account.getId();
String serverUri = account.getServer().getUri();
WebClient webClient = WebClient.builder()
.baseUrl("https://" + serverUri)
.build();
return webClient.get()
.uri("/api/v1/accounts/{id}/statuses?limit=1", id)
.exchangeToMono(clientResponse ->
clientResponse.statusCode().equals(HttpStatus.OK)
? clientResponse
.bodyToMono(String.class)
.map(AccountController::extractData)
: Mono.just("could not retrieve last status")
)
.block();
}
}
將 lastMessage
欄位新增到 schema 中的 Account
並重新啟動應用程式,現在可以查詢具有此額外資訊的帳戶。
具有聯合資料的查詢
{
accounts {
username
lastMessage
}
}
具有聯合資料的回應
{
"data": {
"accounts": [
{
"username": "meistermeier",
"lastMessage": "@taseroth erst einmal schauen, ob auf die Aussage auch Taten folgen ;)"
},
{
"username": "odrotbohm",
"lastMessage": "Some #jMoleculesp/#SpringCLI integration cooking to easily add the former[...]"
},
{
"username": "rotnroll666",
"lastMessage": "Werd aber das Rad im Rückwärts-Turbo schon irgendwie vermissen."
}
]
}
}
再次查看控制器,很明顯現在資料的檢索是一個很大的瓶頸。對於每個 Account
,都會在另一個之後發出請求。但是 Spring for GraphQL 有助於改善每個 Account
之後依序請求的情況。解決方案是在 *lastMessage* 欄位上使用 @BatchMapping
,而不是 @SchemaMapping
。
聯合資料的 BatchMapping
@Controller
public class AccountController {
@BatchMapping
public Flux<String> lastMessage(List<Account> accounts) {
return Flux.concat(
accounts.stream().map(account -> {
var id = account.getId();
String serverUri = account.getServer().getUri();
WebClient webClient = WebClient.builder()
.baseUrl("https://" + serverUri)
.build();
return webClient.get()
.uri("/api/v1/accounts/{id}/statuses?limit=1", id)
.exchangeToMono(clientResponse ->
clientResponse.statusCode().equals(HttpStatus.OK)
? clientResponse
.bodyToMono(String.class)
.map(AccountController::extractData)
: Mono.just("could not retrieve last status")
);
}).toList());
}
}
為了進一步改善這種情況,建議還引入適當的快取到結果中。可能不需要在每次請求時都提取聯合資料,而只需在一段時間後重新整理即可。
Neo4j-Migrations 是一個將遷移應用於 Neo4j 的專案。為了確保資料庫中始終存在乾淨的資料狀態,提供了一個初始的 Cypher 語句。它的內容與本文開頭的 Cypher 片段相同。事實上,內容直接從此檔案中包含。
透過提供 Spring Boot starter 將 Neo4j-Migrations 放在類別路徑上,它將執行來自預設資料夾 (resources/neo4j/migrations) 的所有遷移。
Neo4j-Migrations 相依性定義
<dependency>
<groupId>eu.michael-simons.neo4j</groupId>
<artifactId>neo4j-migrations-spring-boot-starter</artifactId>
<version>${neo4j-migrations.version}</version>
<scope>test</scope>
</dependency>
Spring Boot 3.1 帶來了 Testcontainers 的新功能。其中一個功能是自動設定屬性,而無需定義 @DynamicPropertySource
。在容器啟動後,(Spring Boot 已知的) 屬性將在測試執行時自動填入。
首先,在我們的 *pom.xml* 中需要 Testcontainers Neo4j 的依賴定義。
Testcontainers 依賴定義
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
為了使用 Testcontainers Neo4j,將會建立一個容器定義介面。
容器配置
interface Neo4jContainerConfiguration {
@Container
@ServiceConnection
Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5"))
.withRandomPassword()
.withReuse(true);
}
然後,可以在(整合)測試類別中使用 @ImportTestContainers
註解。
使用 @ImportTestContainers
註解的測試
@SpringBootTest
@ImportTestcontainers(Neo4jContainerConfiguration.class)
class Neo4jGraphqlApplicationTests {
final GraphQlTester graphQlTester;
@Autowired
public Neo4jGraphqlApplicationTests(ExecutionGraphQlService graphQlService) {
this.graphQlTester = ExecutionGraphQlServiceTester.builder(graphQlService).build();
}
@Test
void resultMatchesExpectation() {
String query = "{" +
" account(username:\"meistermeier\") {" +
" displayName" +
" }" +
"}";
this.graphQlTester.document(query)
.execute()
.path("account")
.matchesJson("[{\"displayName\":\"Gerrit Meier\"}]");
}
}
為了完整起見,此測試類別還包含了 GraphQlTester
和一個測試應用程式 GraphQL API 的範例。
現在也可以直接從測試資料夾運行整個應用程式,並使用 Testcontainers 映像。
從測試類別使用容器啟動應用程式
@TestConfiguration(proxyBeanMethods = false)
class TestNeo4jGraphqlApplication {
public static void main(String[] args) {
SpringApplication.from(Neo4jGraphqlApplication::main)
.with(TestNeo4jGraphqlApplication.class)
.run(args);
}
@Bean
@ServiceConnection
Neo4jContainer<?> neo4jContainer() {
return new Neo4jContainer<>("neo4j:5").withRandomPassword();
}
}
@ServiceConnection
註解還負責讓從測試類別啟動的應用程式知道容器運行的座標(連線字串、使用者名稱、密碼...)。
為了在 IDE 之外啟動應用程式,現在也可以調用 ./mvnw spring-boot:test-run
。如果測試資料夾中只有一個帶有 main 方法的類別,它將會被啟動。
除了 QueryByExampleExecutor
之外,Spring Data Neo4j 模組還支援 QuerydslPredicateExecutor
。為了使用它,repository 需要擴展 CrudRepository
而不是 Neo4jRepository
,並且還需要將其宣告為給定類型的 QuerydslPredicateExecutor
。 添加對滾動/分頁的支援,還需要添加 QuerydslDataFetcher.QuerydslBuilderCustomizer
並實現其 customize
方法。
本部落格文章中介紹的整個基礎架構也適用於 reactive stack。基本上,在所有內容前加上 Reactive...
(例如 ReactiveQuerybyExampleExecutor
)將使其變成一個 reactive 應用程式。
最後但同樣重要的是,這裡使用的滾動機制是基於 OffsetScrollPosition
。還有一個 KeysetScrollPosition
可以使用。它結合了排序屬性/屬性和定義的 id。
@Override
default QueryByExampleDataFetcher.Builder<Account, ?> customize(QueryByExampleDataFetcher.Builder<Account, ?> builder) {
return builder.sortBy(Sort.by("username"))
.defaultScrollSubrange(new ScrollSubrange(ScrollPosition.keyset(), 1, true));
}
很高興看到 Spring Data 模組中方便的方法不僅為使用者提供更廣泛的可訪問性,而且還被其他 Spring 專案使用,以減少需要編寫的程式碼量。 這樣可以減少對現有程式碼庫的維護,並有助於專注於業務問題而不是基礎架構。
這篇文章有點長,因為我明確地想至少觸及在調用查詢時發生的事情,而不僅僅是關於自動結果。
請繼續探索更多可能的功能以及應用程式對於不同類型的查詢的行為。 在一篇部落格文章中涵蓋所有主題和功能幾乎是不可能的。
祝您 GraphQL 編碼愉快並探索。 您可以在 GitHub 上的 https://github.com/meistermeier/spring-graphql-neo4j 找到範例專案。