Բոլոր դասերը
ՃարտարապետությունՄիջին11 րոպե ընթերցումԱրժե իմանալ

GraphQL

Սխեմաներ, resolver-ներ, N+1 խնդիրը, caching և անվտանգություն, և երբ REST-ը կամ tRPC-ն են հաղթում։

  • Schemas
  • Resolvers
  • N+1
  • tRPC
  • vs REST
Քայլ 1/5
REST՝ app-ին պետք են user-ը, նրա post-երն ու comment-ները։ Դա երեք endpoint է ու երեք round trip՝ մեկը մյուսի հետևից։

30 վայրկյանում

GraphQL-ը query լեզու է API-ների համար. սերվերը հրապարակում է տիպավորված սխեմա, և յուրաքանչյուր կլիենտ մեկ request-ով խնդրում է ճիշտ այն դաշտերը, որոնք իրեն պետք են։ Այն հիանալի է, երբ շատ տարբեր կլիենտների պետք են նույն տվյալների տարբեր ձևեր։ Այն նաև բարդությունը տեղափոխում է սերվեր. batching՝ N+1 query-ներից խուսափելու համար, ավելի բարդ HTTP caching և պաշտպանություն թանկ query-ներից։

Իմ ամենօրյա stack-ում չէ․ գիտեմ հիմնական գաղափարները և մանրամասները կսովորեմ, երբ նախագիծը դա պահանջի։

Ինչու է սա կարևոր

  • GraphQL-ը կարևոր է, երբ շատ կլիենտների (web, mobile, գործընկերներ) պետք են հարուստ, փոխկապակցված տվյալների մոդելի տարբեր կտորներ։
  • Խիստ տիպավորված սխեման դառնում է պայմանագիր և աղբյուր՝ գեներացվող կլիենտային տիպերի և փաստաթղթավորման համար։
  • Այն կրճատում է over-fetching-ը և under-fetching-ը, ինչն օգնում է mobile կլիենտներին դանդաղ ցանցերում։
  • Դրա արժեքը հասկանալը բացատրում է, թե ինչու ես սովորաբար կառուցում եմ REST API-ներ ընդհանուր, տիպավորված կլիենտներով, և երբ կանցնեի GraphQL-ի։

Ինչպես է աշխատում

01Սխեմաներ, տիպեր և resolver-ներ

Պատկերացրեք REST API-ն որպես ֆիքսված մենյու. յուրաքանչյուր endpoint վերադարձնում է որոշակի ճաշատեսակ։ GraphQL-ը շվեդական սեղան է՝ ճշգրիտ պատվերի ձևաթղթով. կլիենտը նշում է իր ուզած ուտեստներն ու բաժինները։ Սերվերը նկարագրում է ամբողջ հասանելին սխեմայում, որը գրված է Schema Definition Language-ով (SDL)։ Object տիպերը (Product, Order) ունեն տիպավորված դաշտեր. scalar-ները հիմնականներն են (String, Int, Boolean, ID). բացականչական նշանը նշանակում է non-null. ցուցակները գրվում են քառակուսի փակագծերով։ Հատուկ root տիպերը սահմանում են մուտքի կետերը. Query՝ կարդալու, Mutation՝ գրելու, Subscription՝ իրական ժամանակի թարմացումների համար։ Յուրաքանչյուր դաշտի հետևում resolver է՝ ֆունկցիա, որը վերադարձնում է դրա արժեքը։ Սերվերը parse է անում query-ն, validate է անում այն սխեմայի դեմ, հետո դաշտ առ դաշտ կանչում resolver-ները՝ կառուցելով պատասխան, որը կրկնում է query-ի ձևը։ Անհայտ դաշտերը մերժվում են դեռ մինչև որևէ կոդի աշխատելը։

graphql
type Product {
  id: ID!
  name: String!
  price: Int!
  category: Category!
}
 
type Category {
  id: ID!
  name: String!
}
 
type Query {
  products(first: Int = 20, after: String): [Product!]!
  product(id: ID!): Product
}
 
type Mutation {
  renameProduct(id: ID!, name: String!): Product!
}

02Query-ներ, mutation-ներ և subscription-ներ

Query-ն տվյալներ է խնդրում. կլիենտն ընտրում է դաշտերը և կարող է մեկ request-ում հետևել կապերին, օրինակ՝ ապրանքներ իրենց կատեգորիաների անուններով։ Variable-ները query-ները պահում են ստատիկ և վերաօգտագործելի, ինչը նաև հնարավոր է դարձնում persisted query-ները։ Fragment-ները վերաօգտագործում են դաշտերի ընտրությունը կոմպոնենտների միջև, և շատ կլիենտային գրադարաններ դրանք տեղադրում են UI կոմպոնենտների կողքին։ Mutation-ը փոխում է տվյալները և վերադարձնում թարմացված օբյեկտը, որպեսզի կլիենտի cache-ը թարմանա առանց լրացուցիչ request-ի. մեկ request-ի mutation-ները կատարվում են հաջորդաբար։ Mutation-ները նախագծեք բիզնես գործողությունների շուրջ (placeOrder, cancelSubscription), ոչ թե աղյուսակների ընդհանուր թարմացումների, և սպասվող ձախողումների համար, օրինակ validation-ի, վերադարձրեք կառուցվածքային սխալներ payload-ի ներսում։ Subscription-ները թարմացումները push են անում կլիենտին, սովորաբար WebSocket-ներով կամ server-sent event-ներով, օրինակ՝ պատվերի կարգավիճակի կամ chat-ի համար։ Դրանք ավելացնում են stateful կապեր, որոնք պետք է մասշտաբավորել և պաշտպանել, ուստի օգտագործեք դրանք այնտեղ, որտեղ կենդանի թարմացումներն իսկապես կարևոր են։

03N+1 խնդիրը և DataLoader-ը

Քանի որ յուրաքանչյուր դաշտ ունի իր resolver-ը, միամիտ սերվերը կատարում է մեկ query 50 ապրանքների ցուցակի համար, հետո մեկական query յուրաքանչյուր ապրանքի կատեգորիայի համար. 51 query մեկ request-ի համար։ Սա N+1 խնդիրն է, և ներդրված query-ներն այն ավելի են վատացնում։ Ստանդարտ լուծումը batching-ն է DataLoader-ով։ Անմիջապես տվյալ բերելու փոխարեն յուրաքանչյուր resolver loader-ից key է խնդրում. loader-ը հավաքում է event loop-ի նույն tick-ում խնդրված բոլոր key-երը և բերում դրանք մեկ query-ով (WHERE id IN ...), հետո արդյունքները վերադարձնում ճիշտ հերթականությամբ։ Այն նաև cache է անում request-ի սահմաններում, ուստի նույն կատեգորիան բեռնվում է մեկ անգամ։ Loader-ները ստեղծեք յուրաքանչյուր request-ի համար, երբեք գլոբալ, որպեսզի cache արված տվյալները չարտահոսեն օգտատերերի միջև։ Հետևեք resolver-ների ժամանակներին և query-ների քանակին յուրաքանչյուր operation-ի համար՝ regression-ները բռնելու համար։

ts
import DataLoader from "dataloader";
 
// One set of loaders per request, so caching never leaks between users
export function createLoaders(db: Db) {
  return {
    categoryById: new DataLoader<string, Category | null>(async (ids) => {
      const rows = await db.category.findMany({ where: { id: { in: [...ids] } } });
      const byId = new Map(rows.map((c) => [c.id, c]));
      return ids.map((id) => byId.get(id) ?? null);
    }),
  };
}
 
export const resolvers = {
  Product: {
    category: (p: Product, _args: unknown, ctx: Ctx) =>
      ctx.loaders.categoryById.load(p.categoryId),
  },
};

04Caching և անվտանգություն

REST-ը օգտվում է HTTP caching-ից. յուրաքանչյուր URL ռեսուրս է, որը browser-ները, CDN-ները և proxy-ները կարող են cache անել։ GraphQL-ը սովորաբար POST request-ներ է ուղարկում մեկ endpoint-ի, ուստի այդ շերտը վերանում է։ Պատասխանները. նորմալացված կլիենտային cache-եր (Apollo Client, urql, Relay), որոնք օբյեկտները պահում են ըստ տիպի և ID-ի. persisted query-ներ, որոնք ուղարկվում են GET-ով hash-ով, ինչը կրկին հնարավոր է դարձնում CDN caching-ը. և resolver-ների արդյունքների սերվերային caching՝ ըստ օգտատիրոջ զգույշ scope-ով։ Անվտանգությունը նույնքան ուշադրություն է պահանջում։ Կլիենտը կարող է գրել խորը ներդրված կամ հսկայական query, որը սերվերի վրա ահռելի արժենա, ուստի կիրառեք առավելագույն խորության, query-ի բարդության կամ արժեքի սահմանաչափեր, pagination-ի սահմանաչափեր և timeout-ներ։ Rate limit արեք ըստ արժեքի, ոչ միայն request-ների քանակի։ Production-ում մտածեք private API-ների համար introspection-ն անջատելու մասին, կամ, ավելի լավ, թույլ տվեք միայն ձեր սեփական կլիենտների persisted query-ները։ Թույլտվությունը (authorisation) ստուգեք resolver-ներում կամ բիզնես շերտում, անհրաժեշտության դեպքում՝ ըստ դաշտի, երբեք միայն endpoint-ում։

  • Խորության և բարդության սահմաններ, գումարած պարտադիր pagination ցուցակների համար։
  • Persisted query-ներ. ավելի փոքր request-ներ, CDN caching, անվտանգ allow-list։
  • Դաշտի մակարդակի authorisation և ըստ request-ի DataLoader-ներ։

05GraphQL vs REST vs tRPC

Ընտրեք ըստ կլիենտների լանդշաֆտի և թիմի կառուցվածքի։ GraphQL-ը հաղթում է, երբ տարբեր կարիքներով շատ կլիենտներ կիսում են հարուստ, փոխկապակցված դոմեն, երբ պլատֆորմային թիմը սպասարկում է մի քանի պրոդուկտային թիմի, կամ երբ մի քանի backend միավորում եք մեկ graph-ի մեջ (federation)։ REST-ը հաղթում է public API-ների, պարզ ռեսուրսային մոդելների, ֆայլերի upload-ի և download-ի, webhook-ների և այն ամենի համար, ինչը շահում է HTTP caching-ից և ծանոթ գործիքներից. OpenAPI-ով դուք միևնույն է ստանում եք տիպավորված կլիենտներ։ tRPC-ն հաղթում է TypeScript monorepo-ներում, որտեղ նույն թիմն է տիրապետում սերվերին և կլիենտին. procedure-ները կանչվում են ֆունկցիաների պես՝ end-to-end տիպերով և սխեմայի գրեթե զրոյական overhead-ով, բայց այն միայն TypeScript-ի համար է և նախատեսված չէ որպես public, լեզվից անկախ API։ Շատ համակարգեր խառնում են դրանք. REST կամ tRPC ներքին հավելվածների համար, public REST API գործընկերների համար, և GraphQL միայն այնտեղ, որտեղ դրա ճկունությունն արդարացնում է իր բարդությունը։

Օգտագործեք, երբ

  • Մի քանի կլիենտի (web, iOS, Android, գործընկերներ) պետք են նույն փոխկապակցված տվյալների տարբեր ձևեր։
  • Mobile հավելվածներ, որտեղ round-trip-ների և payload-ի չափի կրճատումը կարևոր է։
  • Պլատֆորմային թիմ, որը մի քանի backend ծառայության վրա միասնական graph է տրամադրում։
  • Պրոդուկտային թիմեր, որոնք արագ են iterate անում UI-ի վրա և չեն ուզում ամեն էկրանի համար նոր endpoint։

Մի՛ օգտագործեք, երբ

  • Պարզ CRUD API-ներ կամ public API-ներ, որտեղ REST-ը և HTTP caching-ը ավելի հեշտ են սպառողների համար։
  • TypeScript monorepo, որտեղ մեկ թիմն է տիրապետում երկու կողմին. tRPC-ն կամ տիպավորված REST-ն ավելի պարզ են։
  • Թիմեր, որոնք չունեն ռեսուրս query-ների արժեքի սահմանաչափերը, batching-ը և monitoring-ը կառավարելու համար։

Այլընտրանքներ

  • REST + OpenAPI

    Ռեսուրսակենտրոն, HTTP-ով cache արվող, ունիվերսալ գործիքներ. տիպավորված կլիենտները գեներացրեք spec-ից։

  • tRPC

    End-to-end TypeScript տիպեր առանց սխեմայի շերտի. լավագույնն է մեկ TypeScript codebase-ի ներսում։

  • gRPC

    Արագ բինար պայմանագրեր service-to-service կանչերի համար. անհարմար է անմիջապես browser-ներից։

  • Backend-for-frontend

    Փոքր REST շերտ յուրաքանչյուր կլիենտի համար, որը տվյալները ձևավորում է հենց այդ հավելվածի համար։

Լավագույն պրակտիկաներ

  1. Սխեման նախագծեք դոմեյնի և կլիենտների կարիքների շուրջ, ոչ թե բազայի աղյուսակների։
  2. Յուրաքանչյուր relationship resolver-ի համար օգտագործեք DataLoader՝ ըստ request-ի։
  3. Կիրառեք խորության, բարդության և pagination-ի սահմանաչափեր և դրեք timeout-ներ։
  4. Սեփական (first-party) կլիենտների համար նախընտրեք persisted query-ներ։
  5. Authorisation-ը կատարեք բիզնես շերտում և թեստավորեք դաշտի մակարդակի իրավունքները։
  6. Սխեման զարգացրեք ավելացնելով և դաշտերը @deprecated-ով նշեք նախքան հեռացնելը։

Տարածված սխալներ

  • Թողարկել առանց batching-ի և production-ում հայտնաբերել հարյուրավոր query-ներ մեկ request-ի համար։
  • Բացել անսահմանափակ ցուցակներ և խորը ներդրվածություն՝ հրավիրելով denial-of-service query-ներ։
  • Իրավունքները ստուգել միայն endpoint-ում, ինչի պատճառով ներդրված դաշտերը տվյալներ են արտահոսեցնում։
  • Ստեղծել գլոբալ DataLoader, որը մի օգտատիրոջ տվյալները cache է անում մյուսի համար։
  • Բազայի աղյուսակները մեկ առ մեկ արտացոլել, ինչը ներքին մանրամասները արտահոսեցնում է public պայմանագրի մեջ։

Խորը վերլուծություն

06Կատարման ներքին մեխանիզմները․ nullability և մասնակի արդյունքներ

GraphQL պատասխանը կարող է պարունակել և՛ data, և՛ errors։ Երբ resolver-ը սխալ է նետում, սխալն ավելացվում է errors զանգվածին՝ ձախողված դաշտի path-ով, և այդ դաշտը դառնում է null։ Եթե դաշտը հայտարարված է non-null, null-ը թույլատրված չէ, ուստի null-ը «բարձրանում» է մինչև մոտակա nullable ծնողը՝ հնարավոր է ջնջելով ամբողջ ցանկը կամ ամբողջ data օբյեկտը։ Դա nullability-ն դարձնում է դիզայնի որոշում, ոչ թե զարդարանք։ Դաշտերը նշիր non-null, երբ դրանք իսկապես միշտ առկա են և էժան (id, name), և պահիր nullable, երբ դրանք կախված են մեկ այլ ծառայությունից կամ թույլտվության ստուգումից, որպեսզի մեկ ձախողումը վատացնի մեկ դաշտ, ոչ թե ամբողջ էջը։ Կլիենտները պետք է կարողանան աշխատել մասնակի տվյալների հետ։ Resolver-ների կատարման հերթականությունը նույնպես կարևոր է․ query-ի հարևան դաշտերը կարող են resolve լինել զուգահեռ, իսկ mutation-ի վերին մակարդակի դաշտերը կատարվում են հաջորդաբար։

  • Սխալները կրում են path․ ձախողված դաշտը դառնում է null։
  • Non-null դաշտերը null-ը տարածում են վերև՝ մինչև մոտակա nullable ծնողը։
  • Անկայուն կախվածություններ կանչող դաշտերը դարձրու nullable։

07Արժեքի վերլուծություն և pagination մեծ մասշտաբով

Միայն խորության սահմանները թույլ են․ ծանծաղ query-ն, որը պահանջում է 100 ապրանք, յուրաքանչյուրը 100 կարծիքով, յուրաքանչյուրը 100 հեղինակով, կարող է պահանջել միլիոն օբյեկտ։ Ստատիկ cost analysis-ը յուրաքանչյուր դաշտին տալիս է արժեք, ցանկի դաշտերը բազմապատկում է պահանջված էջի չափով և մերժում է բյուջեից բարձր query-ները մինչև կատարումը։ Արժեքը հրապարակիր պատասխանի extension-ում, որպեսզի կլիենտները տեսնեն այն, և rate limit արա ըստ արժեքի՝ րոպեում։ Pagination-ը դարձրու պարտադիր՝ էջի առավելագույն չափով։ Cursor-ի վրա հիմնված connections պատերնը (edges, node, pageInfo՝ endCursor-ով և hasNextPage-ով) դե ֆակտո ստանդարտ է․ cursor-ները անթափանց են, կայուն են insert-ների դեպքում և խուսափում են խորը OFFSET scan-երից, որոնք դանդաղեցնում են տվյալների բազաները։ Cursor-ում կոդավորիր sort key-ը և ID-ն, իսկ ներքևում օգտագործիր keyset query (WHERE (created_at, id) < (...) ORDER BY created_at DESC, id DESC)։

graphql
type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
}
 
type ProductEdge {
  cursor: String!
  node: Product!
}
 
type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}
 
type Query {
  products(first: Int! = 20, after: String): ProductConnection!
}

08Federation․ մեկ գրաֆ շատ ծառայություններից

Federation-ը թույլ է տալիս մի քանի թիմերի, որոնցից յուրաքանչյուրին պատկանում է իր subgraph-ը, մինչ կլիենտները տեսնում են մեկ supergraph։ Subgraph-ը հայտարարում է entity-ներ key-ով, օրինակ՝ Product, որը նույնականացվում է id-ով։ Մեկ այլ subgraph կարող է ընդլայնել այդ entity-ն իր դաշտերով, օրինակ՝ reviews-ով, և տրամադրում է reference resolver, որը բեռնում է ապրանքի լրացուցիչ տվյալները՝ ունենալով միայն նրա key-ը։ Router-ը կազմում է subgraph-ների սխեմաները, պլանավորում յուրաքանչյուր մուտքային query, կանչում subgraph-ները ճիշտ հերթականությամբ, հաճախ զուգահեռ, և միավորում արդյունքները։ Սա պահպանում է թիմերի ինքնավարությունը՝ կլիենտներին տալով մեկ API։ Ծախսերը․ CI-ում composition-ի ստուգումները դառնում են կրիտիկական, որովհետև մեկ subgraph կարող է կոտրել supergraph-ը, query plan-երը կարող են բաշխվել բազմաթիվ ներքին կանչերի, ուստի entity resolver-ները պետք է batch անեն, և debugging-ը պահանջում է tracing router-ի միջով։ Սա հարմար է մի քանի backend թիմ ունեցող կազմակերպությունների համար․ մեկ թիմի համար դա ավելորդ ծախս է։

09Սխալները որպես տվյալ, input-ների դիզայն և սպեցիֆիկացիայի նոր հնարավորություններ

errors զանգվածն օգտագործիր անսպասելի ձախողումների համար, իսկ սպասվող բիզնես արդյունքները մոդելավորիր սխեմայում։ Mutation-ը կարող է վերադարձնել union, օրինակ՝ PlaceOrderResult = OrderPlaced | OutOfStock | PaymentDeclined, և կլիենտները յուրաքանչյուր դեպք մշակում են fragment-ներով՝ ստանալով տիպավորված դաշտեր ամեն արդյունքի համար։ Input-ները նախագծիր ըստ գործողության․ placeOrder(input: PlaceOrderInput!)-ը արգումենտները պահում է զարգացվող։ GraphQL սպեցիֆիկացիայի 2025-ի սեպտեմբերի խմբագրությունը, առաջինը 2021-ից ի վեր, ավելացրեց OneOf input օբյեկտներ, որտեղ պետք է տրվի ճիշտ մեկ դաշտ, ինչը կոկիկ մոդելավորում է «գտնել ապրանքը id-ով կամ sku-ով»։ Incremental delivery-ն @defer-ով և @stream-ով, որը թույլ է տալիս պատասխանի դանդաղ մասերին հասնել ավելի ուշ, աջակցվում է մի քանի սերվերների և կլիենտների կողմից, բայց դեռ անցնում է սպեցիֆիկացիայի գործընթացով, ուստի մինչև դրա վրա հենվելը ստուգիր քո stack-ի աջակցությունը։

graphql
union PlaceOrderResult = OrderPlaced | OutOfStock | PaymentDeclined
 
type OrderPlaced { order: Order! }
type OutOfStock { productIds: [ID!]! }
type PaymentDeclined { reason: String! }
 
input ProductLookup @oneOf {
  id: ID
  sku: String
}
 
type Mutation {
  placeOrder(input: PlaceOrderInput!): PlaceOrderResult!
}
 
type Query {
  product(by: ProductLookup!): Product
}

Իրական իրավիճակներ

01

Ապրանքի էջը առաջացնում է 400 հարցում տվյալների բազային

Խնդիր

Նոր էկրանը պահանջում է ապրանքներ՝ կատեգորիաներով, տարբերակներով և կարծիքներով։ Մոնիթորինգը ցույց է տալիս հարյուրավոր հարցումներ մեկ request-ի համար և երկու վայրկյանից բարձր p95 latency։

Ինչպես լուծել

Ավելացրու tracing ըստ resolver-ի՝ տեսնելու համար, թե որ դաշտերն են բաշխվում։ Ներմուծիր DataLoader-ներ, որոնք ստեղծվում են յուրաքանչյուր request-ի համար context-ում, ամեն կապի համար․ կատեգորիա ըստ ID-ի, տարբերակներ ըստ product ID-ի, կարծիքներ ըստ product ID-ի (վերադարձնելով key-ով խմբավորված զանգվածներ)։ Ստուգիր, որ ցանկի դաշտերն ունեն pagination՝ ողջամիտ default-ով։ Նորից չափիր հարցումների քանակը և latency-ն ըստ operation name-ի։ Ավելացրու CI թեստ, որը գործարկում է հիմնական գործողությունները seed արված տվյալների բազայի դեմ և ձախողվում է, եթե հարցումների քանակը գերազանցում է շեմը, որպեսզի ռեգրեսիան լուռ չվերադառնա։

02

Չարաշահող query-ներ հանրային endpoint-ի դեմ

Խնդիր

Ինչ-որ մեկն ուղարկում է խորը ներդրված query-ներ, որոնք API-ն timeout են անում բոլորի համար։ API-ն օգտագործվում է միայն ընկերության սեփական վեբ և մոբայլ հավելվածների կողմից։

Ինչպես լուծել

Քանի որ այն օգտագործում են միայն first-party կլիենտները, անցիր persisted query-ների․ build-ի ժամանակ դուրս բեր գործողությունները, գրանցիր դրանց hash-երը և պրոդակշնում մերժիր allow-list-ում չեղած ցանկացած գործողություն։ Մինչ դա թողարկվի, ավելացրու խորության և արժեքի սահմաններ, էջի առավելագույն չափեր և կատարման timeout, և rate limit արա ըստ արժեքի՝ յուրաքանչյուր կլիենտի համար։ Անջատիր դաշտերի առաջարկները անհայտ դաշտերի սխալի հաղորդագրություններում։ Մերժված query-ները լոգավորիր կլիենտի նույնացուցիչներով՝ տեսնելու, թե արդյոք դա չարաշահում է, թե bug քո սեփական հավելվածում։

03

Լայնորեն օգտագործվող դաշտի հեռացում

Խնդիր

Product.price դաշտը ամբողջ թիվ է ցենտերով, և թիմն ուզում է Money տիպ՝ արժույթով։ Մոբայլ հավելվածի հին տարբերակները ամիսներով կմնան օգտագործման մեջ։

Ինչպես լուծել

Հինի կողքին ավելացրու նոր դաշտ՝ priceMoney: Money!, և price-ը նշիր @deprecated(reason: "Use priceMoney")-ով։ Թողարկիր կլիենտներ, որոնք օգտագործում են նոր դաշտը։ Հետևիր price-ի օգտագործմանը ըստ operation name-ի և կլիենտի տարբերակի՝ սերվերի մետրիկաներում կամ schema registry-ում։ Երկու դաշտերն էլ պահիր, մինչև հինի օգտագործումը իջնի համաձայնեցված մակարդակի՝ հաշվի առնելով մոբայլ հավելվածի պարտադիր թարմացման քաղաքականությունը։ Հետո հեռացրու այն՝ CI-ի schema check-ով, որը հաստատում է, որ ոչ մի գրանցված գործողություն այն այլևս չի պահանջում։

Արագ ուղեցույց

Դաշտը deprecated նշել
price: Int @deprecated(reason: "Use priceMoney")
Cursor pagination-ի արգումենտներ
products(first: Int! = 20, after: String): ProductConnection!
Loader յուրաքանչյուր request-ի համար
context: () => ({ loaders: createLoaders(db) })
Տիպավորված բիզնես սխալներ
union PlaceOrderResult = OrderPlaced | OutOfStock
Ճիշտ մեկ input դաշտ
input ProductLookup @oneOf { id: ID sku: String }
Persisted query (APQ) հարցում
extensions: { persistedQuery: { version: 1, sha256Hash: "..." } }
Դաշտերի ընտրության կրկնակի օգտագործում
fragment ProductCard on Product { id name price }
Federated entity-ի key
type Product @key(fields: "id") { id: ID! }
Սխեմայի ուսումնասիրում
query { __schema { types { name } } }

Հարցեր և պատասխաններ

Q1Ի՞նչ խնդիր է լուծում GraphQL-ը REST-ի համեմատ։

Այն լուծում է over-fetching-ը և under-fetching-ը։ Ֆիքսված REST endpoint-ների դեպքում էկրանը հաճախ ստանում է դաշտեր, որոնք իրեն պետք չեն, կամ պետք է կանչի մի քանի endpoint՝ իր տվյալները հավաքելու համար։ GraphQL-ը թույլ է տալիս կլիենտին մեկ round-trip-ով խնդրել ճիշտ այն դաշտերն ու կապերը, որոնք իրեն պետք են՝ validate արված տիպավորված սխեմայի դեմ։ Դա ամենաշատն օգնում է, երբ շատ տարբեր կլիենտներ սպառում են նույն դոմեյնը։ Փոխզիջումն այն է, որ բարդությունը տեղափոխվում է սերվեր. տվյալների հասանելիության batching, query-ի արժեքի վերահսկում և HTTP caching-ի փոխարինում կլիենտային cache-երով կամ persisted query-ներով։

Q2Ի՞նչ է N+1 խնդիրը, և ինչպե՞ս եք այն լուծում։

Քանի որ յուրաքանչյուր դաշտ ունի իր resolver-ը, N տարրերի ցուցակը, ապա յուրաքանչյուր տարրի համար կապված օբյեկտը բերելը առաջացնում է մեկ query ցուցակի համար գումարած N query կապերի համար։ Ներդրվածության դեպքում դա բազմապատկվում է։ Ստանդարտ լուծումը DataLoader-ն է. resolver-ները key-եր են խնդրում loader-ից, որը հավաքում է նույն tick-ում խնդրված բոլոր key-երը և բեռնում դրանք մեկ batch query-ով, հետո արդյունքները վերադարձնում հերթականությամբ։ Այն նաև cache է անում request-ի ներսում։ Loader-ները պետք է ստեղծվեն յուրաքանչյուր request-ի համար, որպեսզի տվյալներ չարտահոսեն օգտատերերի միջև։ Որոշ framework-եր որպես այլընտրանք առաջարկում են նաև query planning կամ join-ի վրա հիմնված lookahead։

Q3Ինչպե՞ս եք պաշտպանում GraphQL API-ն։

Մի քանի շերտով։ Authentication՝ մուտքի կետում, բայց authorisation՝ բիզնես շերտում կամ ըստ դաշտի, որովհետև մեկ query-ն կարող է հասնել շատ տիպերի։ Սահմանաչափեր թանկ query-ների դեմ. առավելագույն խորություն, բարդության կամ արժեքի գնահատում, պարտադիր pagination՝ էջի առավելագույն չափով, և կատարման timeout-ներ։ Rate limiting՝ ըստ query-ի արժեքի, ոչ թե request-ների քանակի։ Սեփական կլիենտների համար persisted query-ները գործում են որպես allow-list, այնպես որ կամայական query-ները մերժվում են։ Private API-ների համար introspection-ը կարելի է անջատել, թեև դա obscurity է, ոչ թե պաշտպանություն։ Եվ սովորական կանոնները դեռ գործում են. validate արեք input-ը, խուսափեք injection-ից resolver-ներում և լոգավորեք սխալները՝ առանց ներքին մանրամասներ բացահայտելու։

Q4Ինչպե՞ս եք cache անում GraphQL պատասխանները։

HTTP caching-ն ավելի բարդ է, որովհետև query-ները սովորաբար POST-եր են մեկ endpoint-ի՝ տարբեր body-ներով։ Կլիենտում նորմալացված cache-երը, ինչպիսիք են Apollo Client-ը կամ Relay-ը, պահում են օբյեկտները ըստ տիպի և ID-ի և թարմացնում դրանք mutation-ների արդյունքներից։ Persisted query-ները թույլ են տալիս կլիենտներին hash ուղարկել GET-ով, ինչը public տվյալների համար կրկին հնարավոր է դարձնում CDN և browser caching-ը։ Սերվերում թանկ resolver-ների արդյունքները կամ upstream կանչերը cache եք անում Redis-ում՝ key-երը զգուշորեն կապելով օգտատիրոջ կամ tenant-ի հետ, երբ տվյալները private են։ Որոշ սերվերներ աջակցում են ըստ դաշտի cache hint-եր՝ պատասխանի մակարդակի անվտանգ cache policy հաշվարկելու համար։

Q5Ե՞րբ կընտրեիք tRPC-ն GraphQL-ի կամ REST-ի փոխարեն։

Երբ նույն թիմն է տիրապետում TypeScript backend-ին և TypeScript կլիենտներին մեկ repository-ում, օրինակ՝ Next.js հավելված իր API-ով։ tRPC-ն տալիս է end-to-end type safety գրեթե առանց ծիսակարգի. procedure-ները սահմանում եք սերվերում և կանչում կլիենտից ֆունկցիաների պես, և տիպերի սխալներն անմիջապես երևում են։ Չկա սխեմայի լեզու կամ code generation։ Այն հարմար չէ public API-ների, ոչ TypeScript սպառողների կամ շատ անկախ թիմերի համար, որովհետև պայմանագիրը TypeScript տիպերն են, ոչ թե լեզվից անկախ սխեմա։ Այդ դեպքերի համար կընտրեի REST՝ OpenAPI-ով, կամ GraphQL։

Q6Ինչպե՞ս եք զարգացնում GraphQL սխեման առանց versioning-ի։

GraphQL-ը խրախուսում է շարունակական, ավելացնող զարգացում՝ v1 և v2 endpoint-ների փոխարեն։ Նոր տիպեր և դաշտեր ավելացնելն անվտանգ է, որովհետև գոյություն ունեցող կլիենտները դրանք չեն խնդրում։ Դաշտը փոխելու համար ավելացրեք նորը, հինը նշեք @deprecated-ով և պատճառով, և հետևեք դաշտի օգտագործմանը operation tracking-ի միջոցով, մինչև որևէ կլիենտ այն այլևս չօգտագործի, հետո հեռացրեք։ Երբեք մի փոխեք դաշտի իմաստը և nullable արգումենտը մի դարձրեք պարտադիր առանց անցումային փուլի։ CI-ում սխեմայի ստուգումները, որոնք համեմատում են առաջարկվող սխեման production տրաֆիկի հետ, բռնում են breaking change-երը նախքան թողարկումը։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կներդնեիր GraphQL գոյություն ունեցող REST ծառայություններով կազմակերպությունում։

Աստիճանաբար՝ որպես շերտ կլիենտների համար, ոչ թե վերագրում։ Սկսիր մեկ պրոդուկտային մակերեսից, սովորաբար մոբայլ հավելվածից, որը տառապում է բազմաթիվ round-trip-ներից, և կառուցիր GraphQL gateway, որի resolver-ները կանչում են գոյություն ունեցող REST ծառայությունները՝ DataLoader-ներով կանչերը batch անելով և դանդաղների առջև քեշով։ Սխեման նախագծիր կլիենտների կարիքների շուրջ, ոչ թե ներկայիս endpoint-ների ձևով։ Persisted query-ները, արժեքի սահմանները, tracing-ը և schema check-երը ներդրիր առաջին օրվանից։ Եթե pilot-ը նվազեցնում է կլիենտի բարդությունը և latency-ն, ընդլայնիր ըստ դոմեյնի․ երբ մի քանի backend թիմեր սկսեն մասնակցել, մտածիր federation-ի մասին, որպեսզի յուրաքանչյուրին պատկանի իր subgraph-ը։ Փոխզիջումները․ ևս մեկ hop և runtime գործարկելու համար, թիմում հմտությունների բաց և գայթակղություն՝ բիզնես տրամաբանությունը տեղափոխել resolver-ներ, մինչդեռ այն պետք է մնա ծառայություններում։

S2Ինչպե՞ս ես անում field-level authorisation՝ առանց resolver-ները խառնաշփոթի վերածելու։

Authorisation-ը պահիր դոմեյնի շերտում, իսկ GraphQL շերտը թող միայն կանչի այն։ Resolver-ները տվյալներ են վերցնում ծառայությունների միջոցով, որոնք արդեն context-ից գիտեն ընթացիկ օգտատիրոջը կամ tenant-ին և կիրառում են նույն policy-ները, ինչ REST endpoint-ները, այնպես որ կանոններն ապրում են մեկ տեղում։ Դաշտի մակարդակի կանոնների համար, օրինակ՝ ինքնարժեքը տեսանելի է միայն մենեջերներին, օգտագործիր schema directive կամ middleware, որը դաշտերը քարտեզագրում է թույլտվությունների անուններին և ստուգում դրանք մինչև resolve-ը՝ վերադարձնելով null սխալով կամ տիպավորված «թույլատրված չէ» արդյունք։ Համոզվիր, որ batch loader-ներն էլ կիրառում են scoping, քանի որ դրանք շրջանցում են per-object resolver-ները։ Թեստավորիր տարբեր դերերով գործարկվող գործողություններով։ Փոխզիջումը․ դեկլարատիվ directive-ները կոկիկ են, բայց կարող են թաքցնել տրամաբանությունը, ուստի բարդ, տվյալներից կախված կանոնները պահիր ծառայություններում, իսկ directive-ներն օգտագործիր պարզ դերային ստուգումների համար։

S3Apollo-ի ոճի normalised caching, թե՞ պարզ request caching կլիենտում․ ե՞րբ է յուրաքանչյուրն ավելի լավ։

Normalised cache-ը յուրաքանչյուր օբյեկտ պահում է մեկ անգամ՝ ըստ տիպի և ID-ի, այնպես որ թարմացված ապրանք վերադարձնող mutation-ը անմիջապես թարմացնում է այն ցուցադրող բոլոր էկրանները, իսկ համընկնող query-ները կարող են սպասարկվել առանց ցանցային կանչի։ Այն փայլում է հարուստ, ինտերակտիվ հավելվածներում՝ նույն entity-ների բազմաթիվ view-երով։ Դրա գինը բարդությունն է․ պետք է տրամադրես կայուն ID-ներ, գրես քեշի թարմացման տրամաբանություն ցանկերում ավելացումների և ջնջումների համար և debug անես հնացած գրառումները։ Request-level caching-ը, query-ով և variables-ով key-ավորված, ինչպես React Query-ում կամ urql-ի document cache-ում, ավելի պարզ է և կանխատեսելի․ mutation-ներից հետո refetch կամ invalidate արա ըստ query-ի։ Այն հարմար է հիմնականում ընթերցման վրա կենտրոնացած, անկախ էկրաններով հավելվածներին։ Ես ընտրում եմ normalisation, երբ նույն entity-ները խմբագրվում և ցուցադրվում են բազմաթիվ view-երում, իսկ մնացած դեպքերում՝ պարզ caching։

Բառարան

Schema
Տիպավորված նկարագրություն այն բոլոր տիպերի, դաշտերի և գործողությունների, որոնք առաջարկում է GraphQL API-ն։
Resolver
Ֆունկցիա, որը վերադարձնում է տիպի մեկ դաշտի արժեքը։
DataLoader
Գործիք, որը մեկ request-ի ներսում batch և քեշ է անում key-երով որոնումները։
Fragment
Տիպի դաշտերի կրկին օգտագործվող ընտրություն, որը կիսվում է query-ների և կոմպոնենտների միջև։
Persisted query
Նախապես գրանցված գործողություն, որը կլիենտը կանչում է ID-ով կամ hash-ով՝ ամբողջ տեքստն ուղարկելու փոխարեն։
Connection
Cursor-ի վրա հիմնված pagination պատերն՝ edges-ով, node-երով և pageInfo-ով։
Introspection
Query-ներ, որոնք թույլ են տալիս կլիենտներին runtime-ում բացահայտել սխեմայի տիպերն ու դաշտերը։
Federation
Մի քանի անկախ պատկանող subgraph-ների կազմում մեկ supergraph-ի մեջ router-ի հետևում։
Null propagation
Non-null դաշտի սխալը պատասխանում null է դարձնում նրա մոտակա nullable ծնողը։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։