Reconsidération de l'implémentation de la pagination par style Relay dans GraphQL (version utilisant la fonction Window)

thème

Auparavant, j'ai implémenté une implémentation d'essai de Relay style dans l'article suivant.

Cependant, la combinaison des exigences de "passer à la page précédente / suivante" et de "trier par ordre croissant / décroissant par n'importe quel élément" a provoqué une implémentation plus compliquée que je ne l'avais imaginé, et c'était assez indigeste. Cette fois, j'ai essayé de simplifier l'implémentation du côté back-end (de l'époque précédente) en mettant une contrainte architecturale que le RDB à utiliser soit PostgreSQL.

Langues, bibliothèques, etc. utilisées dans cet exemple d'implémentation

De plus, nous n'expliquerons pas ces langues et bibliothèques individuelles.

l'extrémité avant

Identique à Dernier article frontal.

Back end

Autre

Spécifications supposées

Il a les fonctions suivantes sur la page qui répertorie certaines informations (cette fois «Client» (client)).

--Filtre de recherche de chaîne de caractères (recherche de correspondance partielle) --Transition entre la page précédente et la page suivante --Trier par ordre croissant ou décroissant par élément d'affichage de liste

Récupérez simplement tous les éléments lorsque la page initiale est affichée et recherchez à chaque fois (autant que nécessaire pour une page) au lieu de la transition de page précédente et de page suivante en mémoire. Lorsque ce qui suit est exécuté, l'affichage revient à la première page même si la page est affichée (par exemple, la deuxième page est affichée).

--Trier par ordre croissant ou décroissant par élément d'affichage de liste

image d'écran

Lorsque la page initiale est affichée (par défaut, les spécifications sont classées par ordre décroissant d'ID)

screenshot-localhost_3000-2020.11.15-23_36_44.png

Au moment du passage à la deuxième page

screenshot-localhost_3000-2020.11.16-00_59_48.png

Lors de l'utilisation d'un filtre de recherche

screenshot-localhost_3000-2020.11.16-01_01_41.png screenshot-localhost_3000-2020.11.16-01_02_42.png

Trier par nom dans l'ordre croissant

1ère page screenshot-localhost_3000-2020.11.16-01_03_40.png 2ème page screenshot-localhost_3000-2020.11.16-01_03_54.png 3e page screenshot-localhost_3000-2020.11.16-01_04_06.png

Changement du nombre d'éléments affichés dans la liste à 10 (+ "Âge" par ordre décroissant)

1ère page screenshot-localhost_3000-2020.11.16-01_06_09.png

2ème page screenshot-localhost_3000-2020.11.16-01_06_21.png

Index des articles associés

--12e "Réexamen de l'implémentation de la pagination par style de relais dans GraphQL (version d'utilisation des fonctions de fenêtre)" --11e "Réponse au problème N + 1 à l'aide de chargeurs de données"

Environnement de développement

OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"

#Backend

#Langue --Golang

$ go version
go version go1.15.2 linux/amd64

gqlgen

v0.13.0

IDE - Goland

GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020

Toutes les sources cette fois

https://github.com/sky0621/study-graphql/tree/v0.10.0/try01

Entraine toi

DB Exécutez PostgreSQL v13 avec Docker Compse. (Comme il n'est utilisé que localement, écrivez le mot de passe, etc. sous forme solide)

docker-compose.yml


version: '3'

services:
  db:
    restart: always
    image: postgres:13-alpine
    container_name: study-graphql-postgres-container
    ports:
      - "25432:5432"
    environment:
      - DATABASE_HOST=localhost
      - POSTGRES_DB=study-graphql-local-db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=yuckyjuice
      - PGPASSWORD=yuckyjuice
    volumes:
      - ./local/data:/docker-entrypoint-initdb.d/

Créez une table " customer "dans le DB ci-dessus.

CREATE TABLE customer (
  id bigserial NOT NULL,
  name varchar(64) NOT NULL,
  age int NOT NULL,
  PRIMARY KEY (id)
);

Les enregistrements dans la table «client» sont ci-dessous.

Schéma GraphQL

En fait, ce n'est pas si compliqué si c'est juste la partie Relay, mais cette fois c'est une combinaison de "filtre de recherche de chaîne de caractères" et de "tri croissant / décroissant par chaque élément", donc la définition est un peu compliquée.

$ tree schema/
schema/
├── connection.graphql
├── customer.graphql
├── order.graphql
├── pagination.graphql
├── schema.graphql
└── text_filter.graphql

■schema.graphql

# Global Object Identification ...Toutes les données uniques avec un identifiant commun
interface Node {
    id: ID!
}

schema {
    query: Query
}

type Query {
    node(id: ID!): Node
}

■customer.graphql

CustomerConnection Requête

extend type Query {
  "Obtenir la liste TODO par recherche compatible avec la pagination compatible Relay"
  customerConnection(
    "Conditions de pagination"
    pageCondition: PageCondition
    "Conditions de tri"
    edgeOrder: EdgeOrder
    "Condition de filtre de chaîne"
    filterWord: TextFilterCondition
  ): CustomerConnection
}

Il s'agit de la requête appelée cette fois depuis le front-end. Une description de chaque élément sera décrite plus loin. Il a les champs suivants selon les exigences.

La valeur de retour de la requête est dans un format de connexion compatible relais (également décrit plus loin).

CustomerConnection

"Pour renvoyer des résultats avec pagination"
type CustomerConnection implements Connection {
  "Informations sur la page"
  pageInfo: PageInfo!
  "Liste des résultats de la recherche (* y compris les informations sur le curseur)"
  edges: [CustomerEdge!]!
  "Nombre total de résultats de recherche"
  totalCount: Int64!
}

Pour stocker le résultat de l'exécution de la requête customerConnection. Conforme aux Spécifications des relais (peut-être pas au niveau, niveau de référence).

L'interface Connection (décrite plus loin) est implémentée de manière à pouvoir être utilisée à des fins générales. Les informations sur la page («PageInfo») seront décrites plus tard.

CustomerEdge

"Résultats de la recherche (* y compris les informations sur le curseur)"
type CustomerEdge implements Edge {
  node: Customer!
  cursor: Cursor!
}

L'interface Edge (décrite plus loin) est implémentée de manière à pouvoir être utilisée à des fins générales. Affiche les résultats de la recherche pour un élément. Il contient des informations appelées «curseur» pour l'identification des données. Le type "Cursor" sera décrit plus loin.

Customer

type Customer implements Node {
  "ID"
  id: ID!
  "Nom"
  name: String!
  "âge"
  age: Int!
}

Représente un client.

■pagination.graphql PageCondition Un type qui représente la «condition de pagination» transmise à la requête.

"Conditions de pagination"
input PageCondition {
    "Condition de transition de la page précédente"
    backward: BackwardPagination
    "Condition de transition de la page suivante"
    forward: ForwardPagination
    "Numéro de la page actuelle (à partir du moment avant cette pagination)"
    nowPageNo: Int64!
    "Nombre d'éléments affichés par page"
    initialLimit: Int64!
}

BackwardPagination Condition de pagination réussie au moment de la transition "page précédente".

"Condition de transition de la page précédente"
input BackwardPagination {
    "Nombre d'acquisitions"
    last: Int64!
    "Curseur d'identification de la cible d'acquisition (* Les enregistrements avant ce curseur au moment de la transition vers la page précédente sont des cibles d'acquisition)"
    before: Cursor!
}

ForwardPagination La condition de pagination est passée lors de la transition "page suivante".

"Condition de transition de la page suivante"
input ForwardPagination {
    "Nombre d'acquisitions"
    first: Int64!
    "Curseur d'identification de la cible d'acquisition (* Les enregistrements derrière ce curseur sont acquis lors du passage à la page suivante)"
    after: Cursor!
}

Cursor Dans le curseur, la valeur encodée en URL est stockée après avoir combiné le ROW_NUMBER qui est attribué lors de la recherche dans la base de données avec le nom de la table. Voir ci-dessous.

"Curseur (identifiant qui identifie de manière unique un enregistrement)"
scalar Cursor

■order.graphql EdgeOrder Un type qui représente la "condition de tri" passée à la requête.

"Conditions de tri"
input EdgeOrder {
    "Trier l'élément clé"
    key: OrderKey!
    "Direction du tri"
    direction: OrderDirection!
}

OrderKey

"""
Clé de tri

[Processus d'examen]
Je voulais en faire une structure polyvalente et sécurisée, j'ai donc essayé de l'implémenter avec une entrée ou une énumération pour chaque fonction après l'avoir définie avec l'interface.
Cependant, j'ai abandonné parce que l'entrée était une spécification qui ne pouvait pas implémenter l'interface.
Je souhaite enum avoir une fonction d'héritage, mais ce n'est pas le cas.
En union, CustomerOrderKey et (si plus) trient les clés pour d'autres fonctionnalités|J'ai pensé à comment me connecter avec
J'ai également abandonné parce que c'était une spécification que l'union ne pouvait pas être incluse comme élément dans l'entrée.
Cependant, je voulais fournir le tri comme mécanisme commun, et par conséquent, j'ai énuméré les champs d'énumération pour chaque fonction dans une entrée commune.
"""
input OrderKey {
    "Clé de tri pour la liste des utilisateurs"
    customerOrderKey: CustomerOrderKey
}

OrderDirection

"Direction de tri"
enum OrderDirection {
    "ordre croissant"
    ASC
    "Ordre décroissant"
    DESC
}

■text_filter.graphql TextFilterCondition Type qui représente la «condition de filtre de chaîne de caractères» à transmettre à la requête.

"Condition de filtre de chaîne"
input TextFilterCondition {
    "Filtrer la chaîne"
    filterWord: String!
    "Motif assorti"
    matchingPattern: MatchingPattern!
}

MatchingPattern

"Type de modèle correspondant (* Ajouter "début de correspondance" ou "fin de correspondance" selon les besoins)"
enum MatchingPattern {
    "Match partiel"
    PARTIAL_MATCH
    "Correspondance parfaite"
    EXACT_MATCH
}

■connection.graphql

scalar Int64

"Pour renvoyer des résultats avec pagination"
interface Connection {
    "Informations sur la page"
    pageInfo: PageInfo!
    "Liste de résultats (* avec informations sur le curseur)"
    edges: [Edge!]!
    "Nombre total de résultats de recherche"
    totalCount: Int64!
}

"Informations sur la page"
type PageInfo {
    "Avec ou sans page suivante"
    hasNextPage: Boolean!
    "Avec ou sans page précédente"
    hasPreviousPage: Boolean!
    "1er enregistrement de la page"
    startCursor: Cursor!
    "Dernier enregistrement sur la page"
    endCursor: Cursor!
}

"Liste des résultats de la recherche (* y compris les informations sur le curseur)"
interface Edge {
    "La substitution est possible si le type implémente l'interface Node"
    node: Node!
    cursor: Cursor!
}

Back end

fonction principale

Ce n'est pas le sujet de cette fois, alors présentez simplement la source.

server.go


package main

import (
	"log"
	"net/http"
	"time"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/go-chi/chi"
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
	"github.com/rs/cors"
	"github.com/sky0621/study-graphql/try01/src/backend/graph"
	"github.com/sky0621/study-graphql/try01/src/backend/graph/generated"
	"github.com/volatiletech/sqlboiler/v4/boil"
)

func main() {
	// MEMO:Puisqu'il n'est utilisé que localement, il est solide
	dsn := "host=localhost port=25432 dbname=study-graphql-local-db user=postgres password=yuckyjuice sslmode=disable"
	db, err := sqlx.Connect("postgres", dsn)
	if err != nil {
		log.Fatal(err)
	}

	boil.DebugMode = true

	var loc *time.Location
	loc, err = time.LoadLocation("Asia/Tokyo")
	if err != nil {
		log.Fatal(err)
	}
	boil.SetLocation(loc)

	r := chi.NewRouter()
	r.Use(corsHandlerFunc())
	r.Handle("/", playground.Handler("GraphQL playground", "/query"))
	r.Handle("/query",
		handler.NewDefaultServer(
			generated.NewExecutableSchema(
				generated.Config{
					Resolvers: &graph.Resolver{
						DB: db,
					},
				},
			),
		),
	)

	if err := http.ListenAndServe(":8080", r); err != nil {
		panic(err)
	}
}

func corsHandlerFunc() func(h http.Handler) http.Handler {
	return cors.New(cors.Options{
		AllowedOrigins:   []string{"*"},
		AllowedMethods:   []string{"GET", "POST"},
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
		ExposedHeaders:   []string{"Link"},
		AllowCredentials: true,
		MaxAge:           300, // Maximum value not ignored by any of major browsers
	}).Handler
}

Résolveur d'acquisition de liste de clients prenant en charge la pagination

C'est la source qui porte le sujet de cette époque. Grosso modo,

    1. Définir la structure des paramètres requis pour la construction des instructions SQL pour la recherche
  1. Si la "chaîne de caractères de recherche" est spécifiée depuis le client GraphQL, elle sera reflétée dans la structure ci-dessus.
    1. Si "Paging" est spécifié depuis le client GraphQL (en bref, qu'il s'agisse de l'affichage de la page initiale, de la page précédente ou de la page suivante), il se reflète dans la structure ci-dessus.
  2. Si "Ordre de tri" est spécifié depuis le client GraphQL, il sera reflété dans la structure ci-dessus.
  3. Exécution SQL pour la recherche
  4. Convertir les résultats de la recherche au format relais et les renvoyer

go:graph/customer.resolvers.go(Extrait)


func (r *queryResolver) CustomerConnection(ctx context.Context, pageCondition *model.PageCondition, edgeOrder *model.EdgeOrder, filterWord *model.TextFilterCondition) (*model.CustomerConnection, error) {
	/*
	 *Pour contenir divers éléments nécessaires à la construction SQL
	 */
	params := searchParam{
		//Nom de la table de destination d'acquisition d'informations
		tableName: boiled.TableNames.Customer,

		//L'ordre par défaut est l'ID décroissant
		orderKey:       boiled.CustomerColumns.ID,
		orderDirection: model.OrderDirectionDesc.String(),
	}

	/*
	 *Paramètres de filtre de chaîne de recherche
	 * TODO:Si vous souhaitez appliquer un filtre à plusieurs colonnes, connectez-vous avec AND ici ou buildSearchQueryMod()Besoin d'être envisagé pour se développer
	 */
	filter := filterWord.MatchString()
	if filter != "" {
		params.baseCondition = fmt.Sprintf("%s LIKE '%s'", boiled.CustomerColumns.Name, filter)
	}

	/*
	 *Paramètres de pagination
	 */
	if pageCondition.IsInitialPageView() {
		//Affichage de la page initiale sans pagination
		params.rowNumFrom = 1
		params.rowNumTo = pageCondition.InitialLimit
	} else {
		//Instruction de transition vers la page précédente
		if pageCondition.Backward != nil {
			key, err := decodeCustomerCursor(pageCondition.Backward.Before)
			if err != nil {
				log.Print(err)
				return nil, err
			}
			params.rowNumFrom = key - pageCondition.Backward.Last
			params.rowNumTo = key - 1
		}
		//Instruction de transition vers la page suivante
		if pageCondition.Forward != nil {
			key, err := decodeCustomerCursor(pageCondition.Forward.After)
			if err != nil {
				log.Print(err)
				return nil, err
			}
			params.rowNumFrom = key + 1
			params.rowNumTo = key + pageCondition.Forward.First
		}
	}

	/*
	 *Spécifier la commande
	 */
	if edgeOrder.CustomerOrderKeyExists() {
		params.orderKey = edgeOrder.Key.CustomerOrderKey.String()
		params.orderDirection = edgeOrder.Direction.String()
	}

	/*
	 *Exécution de la recherche
	 */
	var records []*CustomerWithRowNum
	if err := boiled.Customers(buildSearchQueryMod(params)).Bind(ctx, r.DB, &records); err != nil {
		log.Print(err)
		return nil, err
	}

	/*
	 *Nécessaire pour déterminer l'existence de la page suivante et de la page précédente après la pagination
	 *Pour conserver le nombre de résultats après avoir appliqué le filtre de chaîne de caractères de recherche
	 */
	var totalCount int64 = 0
	{
		var err error
		if filter == "" {
			totalCount, err = boiled.Customers().Count(ctx, r.DB)
		} else {
			totalCount, err = boiled.Customers(qm.Where(boiled.CustomerColumns.Name+" LIKE ?",
				filterWord.MatchString())).Count(ctx, r.DB)
		}
		if err != nil {
			log.Print(err)
			return nil, err
		}
	}

	/*
	 *Format de retour de relais
	 */
	result := &model.CustomerConnection{
		TotalCount: totalCount,
	}

	/*
	 *Convertir les résultats de la recherche au format de tranche Edge
	 */
	var edges []*model.CustomerEdge
	for _, record := range records {
		edges = append(edges, &model.CustomerEdge{
			Node: &model.Customer{
				ID:   strconv.Itoa(int(record.ID)),
				Name: record.Name,
				Age:  record.Age,
			},
			Cursor: createCursor("customer", record.RowNum),
		})
	}
	result.Edges = edges

	//Calculez le nombre total de pages de cette recherche à partir du nombre total de résultats de recherche et du nombre d'éléments affichés par page.
	totalPage := pageCondition.TotalPage(totalCount)

	/*
	 *Informations requises pour l'affichage de l'écran et la prochaine pagination côté client
	 */
	pageInfo := &model.PageInfo{
		HasNextPage:     (totalPage - pageCondition.MoveToPageNo()) >= 1, //Y a-t-il encore une page avant la transition?
		HasPreviousPage: pageCondition.MoveToPageNo() > 1,                //Y a-t-il encore une page précédente après la transition?
	}
	if len(edges) > 0 {
		pageInfo.StartCursor = edges[0].Cursor
		pageInfo.EndCursor = edges[len(edges)-1].Cursor
	}
	result.PageInfo = pageInfo

	return result, nil
}

Structure des paramètres requis pour la construction d'une instruction SQL pour la recherche

	params := searchParam{
		//Nom de la table de destination d'acquisition d'informations
		tableName: boiled.TableNames.Customer,

		//L'ordre par défaut est l'ID décroissant
		orderKey:       boiled.CustomerColumns.ID,
		orderDirection: model.OrderDirectionDesc.String(),
	}

L'entité ci-dessus est ci-dessous. En gros, il écrase avec les conditions passées du client GraphQL, mais s'il n'est pas spécifié, il est initialisé au début s'il a besoin d'une valeur par défaut. (Même dans la fonction qui construit l'instruction SQL en passant searchParam, elle est en fait initialisée)

search.go


type searchParam struct {
	orderKey       string
	orderDirection string
	tableName      string
	baseCondition  string
	rowNumFrom     int64
	rowNumTo       int64
}

Paramètres de filtre de chaîne de recherche

	/*
	 *Paramètres de filtre de chaîne de recherche
	 * TODO:Si vous souhaitez appliquer un filtre à plusieurs colonnes, connectez-vous avec AND ici ou buildSearchQueryMod()Besoin d'être envisagé pour se développer
	 */
	filter := filterWord.MatchString()
	if filter != "" {
		params.baseCondition = fmt.Sprintf("%s LIKE '%s'", boiled.CustomerColumns.Name, filter)
	}

La chaîne de caractères pour la recherche est construite par la fonction suivante.

model/expansion.go


func (c *TextFilterCondition) MatchString() string {
	if c == nil {
		return ""
	}
	if c.FilterWord == "" {
		return ""
	}
	matchStr := "%" + c.FilterWord + "%"
	if c.MatchingPattern == MatchingPatternExactMatch {
		matchStr = c.FilterWord
	}
	return matchStr
}

Pour le modèle de correspondance, seules les correspondances exactes et partielles sont préparées pour le moment, mais vous pouvez augmenter la correspondance de préfixe et de suffixe si nécessaire.

model/models_gen.go


//Type de modèle correspondant (* Ajouter "début de correspondance" ou "fin de correspondance" selon les besoins)
type MatchingPattern string

const (
	//Match partiel
	MatchingPatternPartialMatch MatchingPattern = "PARTIAL_MATCH"
	//Correspondance parfaite
	MatchingPatternExactMatch MatchingPattern = "EXACT_MATCH"
)

Paramètres de pagination

Lorsque la page initiale est affichée (en bref, en supposant le moment où l'écran est ouvert pour la première fois, les éléments de tri sont modifiés ou le nombre d'éléments affichés dans la liste est modifié), ce qui suit.

	if pageCondition.IsInitialPageView() {
		//Affichage de la page initiale sans pagination
		params.rowNumFrom = 1
		params.rowNumTo = pageCondition.InitialLimit
	} else {
		〜〜〜
	}

Que ce soit la page initiale ou non est jugé comme suit.

model/expansion.go


func (c *PageCondition) IsInitialPageView() bool {
	if c == nil {
		return true
	}
	return c.Backward == nil && c.Forward == nil
}

Ensuite, la ligne de flux au moment de la transition vers la page précédente ou la page suivante est la suivante.

		〜〜〜
	} else {
		//Instruction de transition vers la page précédente
		if pageCondition.Backward != nil {
			key, err := decodeCustomerCursor(pageCondition.Backward.Before)
			if err != nil {
				log.Print(err)
				return nil, err
			}
			params.rowNumFrom = key - pageCondition.Backward.Last
			params.rowNumTo = key - 1
		}
		//Instruction de transition vers la page suivante
		if pageCondition.Forward != nil {
			key, err := decodeCustomerCursor(pageCondition.Forward.After)
			if err != nil {
				log.Print(err)
				return nil, err
			}
			params.rowNumFrom = key + 1
			params.rowNumTo = key + pageCondition.Forward.First
		}
	}

L'important ici est de décoder le curseur. Le curseur est encodé en URL sous la forme " client + ROW_NUMBER ". ROW_NUMBER est une prémisse selon laquelle un numéro de série est attribué au résultat quel que soit le contenu de la recherche (qu'elle soit réduite ou par ordre croissant ou décroissant).

decodeCustomerCursor(~~~~)

Décodez comme suit.

graph/customer.go


func decodeCustomerCursor(cursor string) (int64, error) {
	modelName, key, err := decodeCursor(cursor)
	if err != nil {
		return 0, err
	}
	if modelName != "customer" {
		return 0, errors.New("not customer")
	}
	return key, nil
}

La définition de decodeCursor (~~~~) est la suivante.

graph/util.go


const cursorSeps = "#####"

func decodeCursor(cursor string) (string, int64, error) {
	byteArray, err := base64.RawURLEncoding.DecodeString(cursor)
	if err != nil {
		return "", 0, err
	}
	elements := strings.SplitN(string(byteArray), cursorSeps, 2)
	key, err := strconv.Atoi(elements[1])
	if err != nil {
		return "", 0, err
	}
	return elements[0], int64(key), nil
}

Voir l'image ci-dessous pour savoir comment obtenir les enregistrements de la page à afficher cette fois avec la logique ci-dessus.

Actuellement, l'état est le suivant.
・ Le nombre d'éléments affichés par page est de 5
・ Organisé par ordre décroissant d'identité
・ L'état où la deuxième page est affichée

1ère page, 2ème page, 3ème page
 ROW_NUMBER: [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]

■ En cas d'instructions pour passer à la "page précédente"
Je veux les enregistrements 1 à 5 sur la première page.
 pageCondition.Backward.ROW décodé avant_En NOMBRE (indique le premier enregistrement sur la deuxième page)[6]Est inclus.
Aussi, pageCondition.Backward.Le nombre d'éléments affichés par page dans Last[5 cas]Est inclus.
Par conséquent, déterminez la plage que vous souhaitez acquérir par le calcul suivant.
 From:6 - 5 = 1
 To :6 - 1 = 5

■ En cas d'instructions pour passer à la "page suivante"
Je veux les enregistrements 11 à 15 sur la troisième page.
 pageCondition.Forward.ROW décodé après_En NUMÉRO (indique le dernier enregistrement sur la deuxième page)[10]Est inclus.
Aussi, pageCondition.Forward.Nombre d'éléments affichés par page dans First[5 cas]Est inclus.
Par conséquent, la plage à acquérir est déterminée par le calcul suivant.
 From:10 + 1 = 11
 To :10 + 5 = 15

Spécifier la commande

	if edgeOrder.CustomerOrderKeyExists() {
		params.orderKey = edgeOrder.Key.CustomerOrderKey.String()
		params.orderDirection = edgeOrder.Direction.String()
	}

La définition de «CustomerOrderKeyExists ()» est la suivante.

model/expansion.go


func (o *EdgeOrder) CustomerOrderKeyExists() bool {
	if o == nil {
		return false
	}
	if o.Key == nil {
		return false
	}
	if o.Key.CustomerOrderKey == nil {
		return false
	}
	return o.Key.CustomerOrderKey.IsValid()
}

Les candidats clés pour le tri lié aux informations «client» sont les suivants.

model/modege_gen.go


type CustomerOrderKey string

const (
	// ID
	CustomerOrderKeyID CustomerOrderKey = "ID"
	//Nom d'utilisateur
	CustomerOrderKeyName CustomerOrderKey = "NAME"
	//âge
	CustomerOrderKeyAge CustomerOrderKey = "AGE"
)

Exécution de la recherche

	var records []*CustomerWithRowNum
	if err := boiled.Customers(buildSearchQueryMod(params)).Bind(ctx, r.DB, &records); err != nil {
		log.Print(err)
		return nil, err
	}

Tout d'abord, la structure de «CustomerWithRowNum», qui est le type de tranche en tant que «enregistrements», est la suivante. boiled.Customer est une structure générée automatiquement par SQL Boiler à partir de la définition de la table DB. Enveloppez ceci et conservez le ROW_NUMBER que vous recevez comme row_num dans votre instruction SQL comme RowNum. En faisant cela, lors de la réception du résultat de l'exécution de l'instruction SQL, il n'est pas nécessaire de créer une structure qui correspond à la définition de table un par un, et seuls les éléments que vous souhaitez ajouter peuvent être ajoutés.

graph/customer.go


type CustomerWithRowNum struct {
	RowNum          int64 `boil:"row_num"`
	boiled.Customer `boil:",bind"`
}

Ensuite, la définition de «buildSearchQueryMod (params)» est la suivante.

graph/search.go


// TODO:Je l'ai fait à peu près pour le moment. Sa polyvalence, telle que la prise en charge de plusieurs tables, dépend des exigences.
func buildSearchQueryMod(p searchParam) qm.QueryMod {
	if p.baseCondition == "" {
		p.baseCondition = "true"
	}
	q := `
		SELECT row_num, * FROM (
			SELECT ROW_NUMBER() OVER (ORDER BY %s %s) AS row_num, *
			FROM %s
			WHERE %s
		) AS tmp
		WHERE row_num BETWEEN %d AND %d
	`
	sql := fmt.Sprintf(q,
		p.orderKey, p.orderDirection,
		p.tableName,
		p.baseCondition,
		p.rowNumFrom, p.rowNumTo,
	)
	return qm.SQL(sql)
}

Utilisez la fonction Fenêtre PostgreSQL (ROW_NUMBER ()) pour attribuer des numéros de série aux résultats de l'application du filtre de recherche et du tri de chaînes spécifiés. À partir du résultat, extrayez la plage souhaitée de ROW_NUMBER. Maintenant, quel que soit l'élément de tri, ordre croissant ou décroissant, vous pouvez obtenir le même mécanisme pour "page précédente" et "page suivante" en spécifiant la plage de ROW_NUMBER.

Convertir les résultats de la recherche au format relais et les renvoyer

Nombre de résultats après application du filtre de chaîne de recherche

Comme indiqué dans le commentaire, le nombre de résultats de la recherche affinée par le filtre de chaîne de caractères de recherche est acquis, et après la transition de page, si la page précédente (suivante) existe toujours (* En renvoyant ces informations, le frontal est sur la conception de l'interface utilisateur , Vous pouvez contrôler l'activation / l'inactivité des boutons [Précédent] et [Suivant].

	/*
	 *Nécessaire pour déterminer l'existence de la page suivante et de la page précédente après la pagination
	 *Pour conserver le nombre de résultats après avoir appliqué le filtre de chaîne de caractères de recherche
	 */
	var totalCount int64 = 0
	{
		var err error
		if filter == "" {
			totalCount, err = boiled.Customers().Count(ctx, r.DB)
		} else {
			totalCount, err = boiled.Customers(qm.Where(boiled.CustomerColumns.Name+" LIKE ?",
				filterWord.MatchString())).Count(ctx, r.DB)
		}
		if err != nil {
			log.Print(err)
			return nil, err
		}
	}

Avec SQL Boiler, écrivez simplement boiled.Customers (). Count (ctx, r.DB) en utilisant la source auto-générée Vous pouvez obtenir le nombre total de tables client. Si vous voulez ajouter une condition de recherche, écrivez-la simplement dans la partiexxxx de boiled.Customers (xxxx)` comme dans la source ci-dessus, en utilisant la méthode de description préparée par SQL Boiler.

Format de retour de relais

Dans le format de retour requis par Relay, tout ce dont vous avez besoin est " bords "et" pageInfo", mais en raison de la conception de l'interface utilisateur, vous voulez généralement le nombre de cas, donc totalCount est également défini. https://relay.dev/graphql/connections.htm#sec-Connection-Types

	/*
	 *Format de retour de relais
	 */
	result := &model.CustomerConnection{
		TotalCount: totalCount,
	}

edges

Le décodage du curseur est comme décrit ci-dessus, mais le codage est ici. Générez un curseur à partir de ROW_NUMBER pour chaque résultat de recherche. En le renvoyant au front-end, la pagination peut être réalisée sur le front-end en ajoutant simplement un curseur au paramètre à la transition de page suivante (sans spécifier la plage d'acquisition en particulier).

	/*
	 *Convertir les résultats de la recherche au format de tranche Edge
	 */
	var edges []*model.CustomerEdge
	for _, record := range records {
		edges = append(edges, &model.CustomerEdge{
			Node: &model.Customer{
				ID:   strconv.Itoa(int(record.ID)),
				Name: record.Name,
				Age:  record.Age,
			},
			Cursor: createCursor("customer", record.RowNum),
		})
	}
	result.Edges = edges

CustomerEdge a la structure suivante.

model/models_gen.go


//Liste des résultats de la recherche (* y compris les informations sur le curseur)
type CustomerEdge struct {
	Node   *Customer `json:"node"`
	Cursor string    `json:"cursor"`
}

La définition de «createCursor (modelName, key)» est la suivante.

graph/util.go


const cursorSeps = "#####"

func createCursor(modelName string, key int64) string {
	return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s%s%d", modelName, cursorSeps, key)))
}

pageInfo

Puisqu'il y a des informations à calculer et à renvoyer ici, le traitement à l'avant est allégé. Les informations suivantes sont requises comme informations sur la page.

model/models_gen.go


//Informations sur la page
type PageInfo struct {
	//Avec ou sans page suivante
	HasNextPage bool `json:"hasNextPage"`
	//Avec ou sans page précédente
	HasPreviousPage bool `json:"hasPreviousPage"`
	//1er enregistrement de la page
	StartCursor string `json:"startCursor"`
	//Dernier enregistrement sur la page
	EndCursor string `json:"endCursor"`
}

Calculez d'abord le «nombre total de pages» pour déterminer «la présence ou l'absence de la page suivante».

	//Calculez le nombre total de pages de cette recherche à partir du nombre total de résultats de recherche et du nombre d'éléments affichés par page.
	totalPage := pageCondition.TotalPage(totalCount)

La définition de «TotalPage (~~)» est la suivante.

model/expansion.go


func (c *PageCondition) TotalPage(totalCount int64) int64 {
	if c == nil {
		return 0
	}
	var targetCount int64 = 0
	if c.Backward == nil && c.Forward == nil {
		targetCount = c.InitialLimit
	} else {
		if c.Backward != nil {
			targetCount = c.Backward.Last
		}
		if c.Forward != nil {
			targetCount = c.Forward.First
		}
	}
	return int64(math.Ceil(float64(totalCount) / float64(targetCount)))
}

En utilisant ce qui précède, «présence ou absence de la page suivante» peut être déterminée comme suit.

	/*
	 *Informations requises pour l'affichage de l'écran et la prochaine pagination côté client
	 */
	pageInfo := &model.PageInfo{
		HasNextPage:     (totalPage - pageCondition.MoveToPageNo()) >= 1, //Y a-t-il encore une page avant la transition?
		HasPreviousPage: pageCondition.MoveToPageNo() > 1,                //Y a-t-il encore une page précédente après la transition?
	}

La définition de MoveToPageNo (), qui est également utilisée dans le jugement "présence / absence de page précédente" ci-dessus, est la suivante.

model/expansion.go


func (c *PageCondition) MoveToPageNo() int64 {
	if c == nil {
		return 1 //Page initiale en raison d'inattendu
	}
	if c.Backward == nil && c.Forward == nil {
		return c.NowPageNo //Parce qu'il ne passe pas à l'avant ou à l'arrière
	}
	if c.Backward != nil {
		if c.NowPageNo <= 2 {
			return 1
		}
		return c.NowPageNo - 1
	}
	if c.Forward != nil {
		return c.NowPageNo + 1
	}
	return 1 //Page initiale en raison d'inattendu
}

Après cela, extrayez le premier et le dernier curseur séparément de l'enregistrement de la page affichée par cette recherche.

	if len(edges) > 0 {
		pageInfo.StartCursor = edges[0].Cursor
		pageInfo.EndCursor = edges[len(edges)-1].Cursor
	}
	result.PageInfo = pageInfo

Ce curseur utilisera " StartCursor "lors de la transition vers la" page précédente "lors de la transition de page suivante dans le front-end, et" EndCursor" lors de la transition vers la "page suivante".

PageCondition
    Backward
Avant ・ ・ ・ StartCursor
    Forward
Après ・ ・ ・ EndCursor

l'extrémité avant

La source est ci-dessous. https://github.com/sky0621/study-graphql/tree/v0.10.0/try01/src/frontend

C'est la même structure que l'article que j'ai écrit auparavant, donc l'explication est omise. Veuillez vous référer à ce qui suit. Implémentation de la pagination par style Relay dans GraphQL (Partie 2: Front end)

Contrôle de fonctionnement

Lorsque la première page est affichée (par ordre décroissant d'ID)

État de la base de données

Screenshot at 2020-11-16 23-12-21.png

Résultat de la transition d'écran

1ère page

screenshot-localhost_3000-2020.11.16-23_17_37.png

2ème page

screenshot-localhost_3000-2020.11.16-23_17_55.png

3e page

screenshot-localhost_3000-2020.11.16-23_18_09.png

Données de réponse GraphQL à la page 3 Screenshot at 2020-11-16 23-21-30.png

Retour à la page 2

screenshot-localhost_3000-2020.11.16-23_24_45.png

Changement dans l'ordre croissant de l'ID

État de la base de données

Screenshot at 2020-11-16 23-25-51.png

Résultat de la transition d'écran

1ère page

screenshot-localhost_3000-2020.11.16-23_26_42.png

2ème page

screenshot-localhost_3000-2020.11.16-23_26_54.png

3e page

screenshot-localhost_3000-2020.11.16-23_27_05.png

Retour à la page 2

screenshot-localhost_3000-2020.11.16-23_27_18.png

Changement dans l'ordre décroissant du nom

État de la base de données

Screenshot at 2020-11-16 23-29-36.png

Résultat de la transition d'écran

1ère page

screenshot-localhost_3000-2020.11.16-23_31_17.png

2ème page

screenshot-localhost_3000-2020.11.16-23_31_29.png

3e page

screenshot-localhost_3000-2020.11.16-23_31_40.png

Retour à la page 2

screenshot-localhost_3000-2020.11.16-23_32_22.png

Ordre croissant de l'âge et passer à "10" par page d'affichage

État de la base de données

Screenshot at 2020-11-16 23-33-46.png

Résultat de la transition d'écran

1ère page

screenshot-localhost_3000-2020.11.16-23_35_09.png

2ème page

screenshot-localhost_3000-2020.11.16-23_35_22.png

Retour à la page 1

screenshot-localhost_3000-2020.11.16-23_35_34.png

Passer au filtre par ordre croissant de nom et " k "

État de la base de données

Screenshot at 2020-11-16 23-39-52.png

Résultat de la transition d'écran

screenshot-localhost_3000-2020.11.16-23_40_35.png

Sommaire

Avec cela, pour le moment, la pagination (et le tri par élément et combinaison de filtres de recherche de chaînes de caractères) sur la page appelée "Liste de clients" a été réalisée. Même en tant qu'implémentation backend, SQL est uniformément dans le même format sans avoir la valeur de l'élément de tri dans Cursor comme précédent. Je peux maintenant frapper. Cependant, la production en série de cela pour chaque fonction est trop pour la plaque chauffante, il est donc nécessaire de créer un modèle autant que possible lors de son utilisation réelle. Aussi, j'écris TODO dans les commentaires de la source, mais il y a divers problèmes.

Recommended Posts

Reconsidération de l'implémentation de la pagination par style Relay dans GraphQL (version utilisant la fonction Window)
Implémentation de la fonction de connexion dans Django
Différence de sortie de la fonction de fenêtre de longueur paire
Apprentissage des classements à l'aide d'un réseau neuronal (implémentation RankNet par Chainer)
Un mémo que j'ai écrit une fonction de base en Python en utilisant la récurrence