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.
De plus, nous n'expliquerons pas ces langues et bibliothèques individuelles.
Identique à Dernier article frontal.
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
1ère page 2ème page 3e page
1ère page
2ème page
--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"
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
$ go version
go version go1.15.2 linux/amd64
v0.13.0
IDE - Goland
GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020
https://github.com/sky0621/study-graphql/tree/v0.10.0/try01
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.
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
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.
Condition de filtre de chaîne
"pour le filtre de recherche de chaîne (correspondance partielle)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!
}
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
}
C'est la source qui porte le sujet de cette époque. Grosso modo,
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
}
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
* 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"
)
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
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"
)
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.
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 partie
xxxx de
boiled.Customers (xxxx)` comme dans la source ci-dessus, en utilisant la méthode de description préparée par SQL Boiler.
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
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)
Données de réponse GraphQL à la page 3
k
"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