Les 28: Grote datasets bijwerken
Soms moeten we duizenden resources in één actie bijwerken, zoals verwoord in de volgende opmerking (geplaatst in een communitygroep over WordPress):
Ik merk dat ik voor veel klanten werk met grote datasets (meer dan 10.000 productvarianten voor 1 product, of meer dan 13.000 mediabestanden)... klanten willen uiteindelijk altijd veel dingen tegelijk in bulk kunnen bewerken — zoals 2000 mediabestanden voorzien van dezelfde tag.
In deze tutoriales gaan we manieren verkennen om deze taak aan te pakken.
Nested Mutations
Om deze GraphQL-query te laten werken, moet de Schemaconfiguratie die op het endpoint is toegepast Nested Mutations hebben ingeschakeld
Dankzij Nested Mutations kunnen we duizenden resources uit de database ophalen en bijwerken via één enkele GraphQL-query:
mutation ReplaceOldWithNewDomainInPosts {
posts(pagination: { limit: 3000 }) {
id
rawContent
adaptedRawContent: _strReplace(
search: "https://my-old-domain.com"
replaceWith: "https://my-new-domain.com"
in: $__rawContent
)
update(input: {
contentAs: { html: $__adaptedRawContent }
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
}
}
}Afhankelijk van de robuustheid van het systeem kan deze enkelvoudige GraphQL-uitvoering echter te veel belasting op de database leggen en zelfs tot een crash leiden.
De uitvoering van de GraphQL-query pagineren
Als het bijwerken van duizenden resources tegelijk het systeem laat crashen, is de oplossing eenvoudig: in plaats van de GraphQL één keer uit te voeren voor duizenden resources, kunnen we hem honderden keren uitvoeren voor tientallen resources per keer.
De volgende bash-scripts bepalen eerst het totale aantal reacties via commentCount, berekenen vervolgens de segmenten op basis van de omgevingsvariabele $ENTRIES_TO_PROCESS, berekenen de pagineringsparameters en roepen de GraphQL-query aan voor elk segment (waarbij alleen de reacties uit dat segment worden opgehaald):
# Get the number of comments in the site
GRAPHQL_RESPONSE=$(curl
-X POST \
-H "Content-Type: application/json" \
-d '{"query": "{\n commentCount\n}"}' \
https://mysite.com/graphql/)
# Extract the number of comments into a variable
COMMENT_COUNT=$(echo $GRAPHQL_RESPONSE \
| grep -E -o '"commentCount\":([0-9]+)' \
| cut -d':' -f2-)
echo "Number of comments: $COMMENT_COUNT"
# How many entries will be processed on each query
ENTRIES_TO_PROCESS=10
# Calculate how many requests must be triggered
PAGINATION_COUNT=$(($(($COMMENT_COUNT / $ENTRIES_TO_PROCESS)) + $(($(($COMMENT_COUNT % $ENTRIES_TO_PROCESS)) ? 1 : 0))))
echo "Number of requests to process (at $ENTRIES_TO_PROCESS entries per request): $PAGINATION_COUNT"
# Execute the requests, at one per second
for PAGINATION_NUMBER in $(seq 0 $(($PAGINATION_COUNT - 1))); do sleep 1 && echo "\n\nPagination number: $PAGINATION_NUMBER\n" && curl -X POST -H "Content-Type: application/json" -d "{\"query\": \"{ comments(pagination: { limit: $ENTRIES_TO_PROCESS, offset: $(($PAGINATION_NUMBER * $ENTRIES_TO_PROCESS)) }) { id date content } }\"}" https://mysite.com/graphql/ ; doneDe GraphQL-query recursief uitvoeren
Omdat de bovenstaande oplossing bash-scripting vereist, moet ze worden uitgevoerd via de CLI (of een beheerpanel of tool), wat het gebruik ervan beperkt.
We kunnen dezelfde logica rechtstreeks in de GraphQL-query inbouwen, zodat we die al binnen WordPress kunnen uitvoeren (en zelfs als GraphQL Persisted Query kunnen opslaan).
De onderstaande GraphQL-query voert zichzelf recursief uit. Wanneer hij voor het eerst wordt aangeroepen:
- Verdeelt hij het totale aantal bij te werken resources in segmenten (berekend aan de hand van de opgegeven variabele
$limit) - Voert hij zichzelf uit via een nieuw HTTP-verzoek voor elk van de segmenten (waarbij de bijbehorende
$offsetals variabele wordt meegegeven), zodat slechts een deel van alle resources tegelijk wordt bijgewerkt
De GraphQL-query is recursief doordat de HTTP-verzoeken wijzen naar dezelfde URL als de huidige (aangevuld met de variabele $offset voor dat segment). De URL (en ook de body, methode en headers) wordt opgehaald uit het huidige HTTP-verzoek (via de extensie HTTP Request via Schema).
Het argument $async dat aan _sendHTTPRequests wordt meegegeven is ingesteld op false, zodat de HTTP-verzoeken één voor één worden uitgevoerd. Daarnaast kun je met de optionele variabele $delay aangeven hoeveel milliseconden er gewacht moet worden voordat elk verzoek wordt verstuurd.
Zodra alle resources zijn bijgewerkt, bereikt de uitvoering van de GraphQL-query het einde en stopt:
# When first invoked, we do not pass variable `$offset`
# Then `$offset` is `null`, and dynamic variable `$executeQuery` will be `true`
query ExportExecute(
$offset: Int
) {
executeQuery: _notNull(value: $offset)
@export(as: "executeQuery")
@remove # Comment this directive to visualize output during development
}
# Only calculate the segments on the first invocation of the GraphQL query
query CalculateVars($limit: Int! = 10)
@depends(on: "ExportExecute")
@skip(if: $executeQuery)
{
# Calculate the number of HTTP requests to be sent
commentCount
fractionalNumberExecutions: _floatDivide(number: $__commentCount, by: $limit)
@remove # Comment this directive to visualize output during development
numberExecutions: _floatCeil(number: $__fractionalNumberExecutions)
# Generate a list of the offset
arrayOffsets: _arrayPad(array: [], length: $__numberExecutions, value: null)
@underEachArrayItem(
passIndexOnwardsAs: "position"
)
@applyField(
name: "_intMultiply"
arguments: {
multiply: $position
with: $limit
}
setResultInResponse: true
)
@export(as: "offsets")
# Vars needed to generate a list of the HTTP Request inputs,
# with many of them retrieved from the current HTTP request data
url: _httpRequestFullURL
@export(as: "url")
@remove # Comment this directive to visualize output during development
method: _httpRequestMethod
@export(as: "method")
@remove # Comment this directive to visualize output during development
headers: _httpRequestHeaders
@remove # Comment this directive to visualize output during development
headersInputList: _objectConvertToNameValueEntryList(
object: $__headers
)
@export(as: "headersInputList")
@remove # Comment this directive to visualize output during development
body: _httpRequestBody
@remove # Comment this directive to visualize output during development
bodyJSONObject: _strDecodeJSONObject(string: $__body)
@export(as: "bodyJSONObject")
@remove # Comment this directive to visualize output during development
bodyHasVariables: _propertyIsSetInJSONObject(
object: $__bodyJSONObject,
by: { key: "variables" }
)
@export(as: "bodyHasVariables")
@remove # Comment this directive to visualize output during development
}
query GenerateVars
@depends(on: ["ExportExecute", "CalculateVars"])
@skip(if: $executeQuery)
{
bodyJSON: _echo(value: $bodyJSONObject)
@unless(condition: $bodyHasVariables)
@objectAddEntry(
key: "variables"
value: {}
)
@export(as: "bodyJSON")
@remove # Comment this directive to visualize output during development
}
# Generate all the HTTPRequestInput objects to send each of the HTTP requests
query GenerateRequestInputs(
$timeout: Float,
$delay: Int
)
@depends(on: ["ExportExecute", "GenerateVars"])
@skip(if: $executeQuery)
{
# Generate a list of the HTTP Request inputs (without the offset)
requestInputs: _echo(value: $offsets)
@underEachArrayItem(
passValueOnwardsAs: "requestOffset"
affectDirectivesUnderPos: [1, 2]
)
@applyField(
name: "_objectAddEntry",
arguments: {
object: $bodyJSON
underPath: "variables"
key: "offset"
value: $requestOffset
},
passOnwardsAs: "itemJSON"
)
@applyField(
name: "_echo",
arguments: {
value: {
url: $url
method: $method
options: {
headers: $headersInputList
json: $itemJSON
timeout: $timeout
delay: $delay
}
}
},
setResultInResponse: true
)
@export(as: "requestInputs")
@remove # Comment this directive to visualize output during development
}
# Execute all the generated URLs, either asynchronously or not
query ExecuteURLs
@depends(on: ["ExportExecute", "GenerateRequestInputs"])
@skip(if: $executeQuery)
{
_sendHTTPRequests(
async: false
inputs: $requestInputs
) {
statusCode
contentType
body
@remove
bodyJSON: _strDecodeJSONObject(string: $__body)
}
}
# This is the actual execution of the query.
# In this case, it simply prints the time when it was executed,
# the provided query variables, and the comment IDs for that segment
query ExecuteQuery(
$offset: Int
$limit: Int! = 10
)
@depends(on: "ExportExecute")
@include(if: $executeQuery)
{
executionTime: _httpRequestRequestTime
queryVariables: _sprintf(string: "[$limit: %s, $offset: %s]", values: [$limit, $offset])
comments(
pagination: { limit: $limit, offset: $offset }
sort: { order: ASC, by: ID }
) {
id
}
}
query ExecuteAll
@depends(on: ["ExecuteURLs", "ExecuteQuery"])
{
id
@remove
}Het antwoord is:
{
"data": {
"commentCount": 23,
"numberExecutions": 3,
"arrayOffsets": [
0,
10,
20
],
"_sendHTTPRequests": [
{
"statusCode": 200,
"contentType": "application/json",
"bodyJSON": {
"data": {
"executionTime": 1689814467,
"queryVariables": "[$limit: 10, $offset: 0]",
"comments": [
{
"id": 2
},
{
"id": 3
},
{
"id": 4
},
{
"id": 5
},
{
"id": 6
},
{
"id": 7
},
{
"id": 8
},
{
"id": 9
},
{
"id": 10
},
{
"id": 11
}
]
}
}
},
{
"statusCode": 200,
"contentType": "application/json",
"bodyJSON": {
"data": {
"executionTime": 1689814468,
"queryVariables": "[$limit: 10, $offset: 10]",
"comments": [
{
"id": 12
},
{
"id": 13
},
{
"id": 16
},
{
"id": 17
},
{
"id": 18
},
{
"id": 19
},
{
"id": 20
},
{
"id": 21
},
{
"id": 22
},
{
"id": 23
}
]
}
}
},
{
"statusCode": 200,
"contentType": "application/json",
"bodyJSON": {
"data": {
"executionTime": 1689814470,
"queryVariables": "[$limit: 10, $offset: 20]",
"comments": [
{
"id": 24
},
{
"id": 25
},
{
"id": 26
}
]
}
}
}
]
}
}