Pelajaran 28: Memperbarui kumpulan data besar
Terkadang kita perlu memperbarui ribuan sumber daya dalam satu tindakan, seperti yang diungkapkan dalam komentar berikut (diposting di grup komunitas tentang WordPress):
Saya menemukan bahwa untuk banyak klien saya bekerja dengan kumpulan data besar (10.000+ variasi produk untuk 1 produk, atau 13.000+ file media) ... pada akhirnya para klien ingin dapat mengedit banyak hal sekaligus secara massal - seperti menandai 2000 file media dengan tag yang sama.
Dalam pelajaran tutorial ini kita akan menjelajahi cara-cara untuk menangani tugas ini.
Nested Mutations
Agar query GraphQL ini berfungsi, Konfigurasi Skema yang diterapkan pada endpoint perlu mengaktifkan Nested Mutations
Berkat Nested Mutations, kita dapat mengambil dan memperbarui ribuan sumber daya dari DB melalui satu query GraphQL:
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
}
}
}
}
}Namun tergantung pada ketahanan sistem, satu eksekusi GraphQL ini mungkin memberikan beban yang terlalu besar pada DB, bahkan membuatnya crash.
Memberi paginasi pada eksekusi query GraphQL
Jika memperbarui ribuan sumber daya sekaligus membuat sistem crash, solusinya sederhana: Daripada mengeksekusi GraphQL hanya sekali untuk ribuan sumber daya, kita dapat mengeksekusinya ratusan kali untuk puluhan sumber daya setiap kalinya.
Skrip bash berikut pertama-tama mencari tahu jumlah total komentar melalui commentCount, kemudian menghitung segmen dengan mempertimbangkan variabel lingkungan $ENTRIES_TO_PROCESS, dan menghitung parameter paginasi serta memanggil query GraphQL untuk setiap segmen (cukup mengambil komentar dari segmen tersebut):
# 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/ ; doneMengeksekusi query GraphQL secara rekursif
Karena solusi di atas melibatkan skrip bash, solusi tersebut harus dieksekusi melalui CLI (atau beberapa panel admin atau alat), yang membatasi penggunaannya.
Kita dapat mereplikasi logika yang sama ke dalam query GraphQL itu sendiri, sehingga memungkinkan kita mengeksekusinya langsung di dalam WordPress (bahkan langsung menyimpannya sebagai GraphQL Persisted Query).
Query GraphQL di bawah ini mengeksekusi dirinya sendiri secara rekursif. Saat pertama kali dipanggil, query ini:
- Membagi jumlah total sumber daya yang akan diperbarui menjadi segmen-segmen (dihitung menggunakan variabel
$limityang disediakan) - Mengeksekusi dirinya sendiri melalui permintaan HTTP baru untuk setiap segmen (meneruskan
$offsetyang sesuai sebagai variabel), sehingga hanya memperbarui sebagian dari semua sumber daya pada suatu waktu
Query GraphQL bersifat rekursif dengan membuat permintaan HTTP mengarah ke URL yang sama dengan URL saat ini (ditambah variabel $offset untuk segmen tersebut), di mana kita mengambil URL tersebut (beserta body, method, dan headers) dari permintaan HTTP saat ini (melalui ekstensi HTTP Request via Schema).
Argumen $async yang diteruskan ke _sendHTTPRequests telah disetel ke false, sehingga permintaan HTTP akan dieksekusi satu per satu. Selain itu, variabel opsional $delay memungkinkan untuk menentukan berapa milidetik penundaan sebelum mengirim setiap permintaan.
Setelah semua sumber daya diperbarui, eksekusi query GraphQL mencapai akhir dan berakhir:
# 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
}Responsnya adalah:
{
"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
}
]
}
}
}
]
}
}